Skip to content

Commit df8eda4

Browse files
Adds AttestationTypeInfo and ClaimTypeInfo classes to obtain claim type information.
Added extension functions to filter claim collections by type or property. Improved `amend` extension function for Corda claims. Added `QueryDsl` extension functions to make vault queries for claims and attestations much simpler and less error prone.
1 parent 86e4412 commit df8eda4

File tree

17 files changed

+630
-153
lines changed

17 files changed

+630
-153
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.onixlabs.corda.identityframework.contract
2+
3+
import io.onixlabs.corda.core.toClass
4+
import io.onixlabs.corda.core.toTypedClass
5+
import java.lang.reflect.ParameterizedType
6+
import java.lang.reflect.Type
7+
import java.lang.reflect.WildcardType
8+
9+
/**
10+
* Represents the base class for implementing objects that obtain underlying attestation and attestation state types.
11+
* This is inspired by TypeReference<T>.
12+
*
13+
* @param T The underlying attestation type.
14+
* @property attestationType Obtains the most derived attestation type specified in the type hierarchy.
15+
* @property attestationStateType Obtains the attestation state type, or null if the value type was specified as a wildcard.
16+
*/
17+
abstract class AttestationTypeInfo<T : Attestation<*>> {
18+
19+
val attestationType: Class<T>
20+
get() = getAttestationTypeInternal().toTypedClass()
21+
22+
val attestationStateType: Class<*>?
23+
get() = getAttestationStateTypeInternal()?.toClass()
24+
25+
/**
26+
* Obtains the most derived claim type specified in the type hierarchy.
27+
*/
28+
private fun getAttestationTypeInternal(): Type {
29+
val superClass = javaClass.genericSuperclass
30+
check(superClass !is Class<*>) { "ClaimTypeInfo constructed without actual type information." }
31+
return (superClass as ParameterizedType).actualTypeArguments[0]
32+
}
33+
34+
/**
35+
* Obtains the claim value type, or null if the value type was specified as a wildcard.
36+
*/
37+
private tailrec fun getAttestationStateTypeInternal(attestationType: Type = getAttestationTypeInternal()): Type? {
38+
return if (attestationType is ParameterizedType) {
39+
val argument = attestationType.actualTypeArguments[0]
40+
if (argument is WildcardType) null else argument
41+
} else getAttestationStateTypeInternal(attestationType.toClass().genericSuperclass)
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.onixlabs.corda.identityframework.contract
2+
3+
import io.onixlabs.corda.core.toClass
4+
import io.onixlabs.corda.core.toTypedClass
5+
import java.lang.reflect.ParameterizedType
6+
import java.lang.reflect.Type
7+
import java.lang.reflect.WildcardType
8+
9+
/**
10+
* Represents the base class for implementing objects that obtain underlying claim and claim value types.
11+
* This is inspired by TypeReference<T>.
12+
*
13+
* @param T The underlying claim type.
14+
* @property claimType Obtains the most derived claim type specified in the type hierarchy.
15+
* @property claimValueType Obtains the claim value type, or null if the value type was specified as a wildcard.
16+
*/
17+
abstract class ClaimTypeInfo<T : AbstractClaim<*>> {
18+
19+
val claimType: Class<T>
20+
get() = getClaimTypeInternal().toTypedClass()
21+
22+
val claimValueType: Class<*>?
23+
get() = getClaimValueTypeInternal()?.toClass()
24+
25+
/**
26+
* Obtains the most derived claim type specified in the type hierarchy.
27+
*/
28+
private fun getClaimTypeInternal(): Type {
29+
val superClass = javaClass.genericSuperclass
30+
check(superClass !is Class<*>) { "ClaimTypeInfo constructed without actual type information." }
31+
return (superClass as ParameterizedType).actualTypeArguments[0]
32+
}
33+
34+
/**
35+
* Obtains the claim value type, or null if the value type was specified as a wildcard.
36+
*/
37+
private tailrec fun getClaimValueTypeInternal(claimType: Type = getClaimTypeInternal()): Type? {
38+
return if (claimType is ParameterizedType) {
39+
val argument = claimType.actualTypeArguments[0]
40+
if (argument is WildcardType) null else argument
41+
} else getClaimValueTypeInternal(claimType.toClass().genericSuperclass)
42+
}
43+
}

onixlabs-corda-identity-framework-contract/src/main/kotlin/io/onixlabs/corda/identityframework/contract/CordaClaimContract.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import java.security.PublicKey
2727

2828
/**
2929
* Represents the smart contract for corda claims.
30-
* This contract is open to allow developers to implement their own corda claim contracts.
30+
* This contract is open to allow developers to implement their own corda claim contracts, however it has been designed
31+
* such that derived contracts will respect and not circumvent the underlying constraints in the parent contract.
3132
*/
3233
open class CordaClaimContract : Contract {
3334

@@ -74,7 +75,7 @@ open class CordaClaimContract : Contract {
7475
}
7576

7677
/**
77-
* Represents the command to amend evolvable claims.
78+
* Represents the command to amend corda claims.
7879
*/
7980
object Amend : CordaClaimContractCommand {
8081
internal const val CONTRACT_RULE_INPUTS =
@@ -100,7 +101,7 @@ open class CordaClaimContract : Contract {
100101
}
101102

102103
/**
103-
* Represents the command to revoke evolvable claims.
104+
* Represents the command to revoke corda claims.
104105
*/
105106
object Revoke : CordaClaimContractCommand {
106107
internal const val CONTRACT_RULE_INPUTS =
@@ -114,23 +115,23 @@ open class CordaClaimContract : Contract {
114115
}
115116

116117
/**
117-
* Provides the ability to extend the rules for issuing evolvable claims.
118+
* Provides the ability to extend the rules for issuing corda claims.
118119
*
119120
* @param transaction The ledger transaction to verify.
120121
* @param signers The signers of the transaction.
121122
*/
122123
protected open fun onVerifyIssue(transaction: LedgerTransaction, signers: Set<PublicKey>) = requireThat { }
123124

124125
/**
125-
* Provides the ability to extend the rules for amending evolvable claims.
126+
* Provides the ability to extend the rules for amending corda claims.
126127
*
127128
* @param transaction The ledger transaction to verify.
128129
* @param signers The signers of the transaction.
129130
*/
130131
protected open fun onVerifyAmend(transaction: LedgerTransaction, signers: Set<PublicKey>) = requireThat { }
131132

132133
/**
133-
* Provides the ability to extend the rules for revoking evolvable claims.
134+
* Provides the ability to extend the rules for revoking corda claims.
134135
*
135136
* @param transaction The ledger transaction to verify.
136137
* @param signers The signers of the transaction.

onixlabs-corda-identity-framework-contract/src/main/kotlin/io/onixlabs/corda/identityframework/contract/Extensions.AbstractClaim.kt

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,16 @@
1616

1717
package io.onixlabs.corda.identityframework.contract
1818

19+
private typealias Claims = Iterable<AbstractClaim<*>>
20+
1921
/**
2022
* Determines whether a collection of [AbstractClaim] contains duplicate properties.
2123
*
2224
* @param property The property to search for duplicates.
2325
* @param ignoreCase Determines whether to ignore case when searching for duplicates.
2426
* @return Returns true if the collection contains duplicates; otherwise, false.
2527
*/
26-
fun Iterable<AbstractClaim<*>>.containsDuplicateProperties(
27-
property: String? = null,
28-
ignoreCase: Boolean = false
29-
): Boolean {
28+
fun Claims.containsDuplicateProperties(property: String? = null, ignoreCase: Boolean = false): Boolean {
3029
val properties = map { if (ignoreCase) it.property.toLowerCase() else it.property }
3130
val filteredProperties = properties.filter { property?.equals(it, ignoreCase) ?: true }
3231
return filteredProperties.size != filteredProperties.distinct().size
@@ -40,8 +39,92 @@ fun Iterable<AbstractClaim<*>>.containsDuplicateProperties(
4039
* @param message The exception message to throw if the collection contains duplicate keys.
4140
* @throws IllegalStateException if the collection contains duplicate keys.
4241
*/
43-
fun Iterable<AbstractClaim<*>>.checkForDuplicateProperties(
42+
fun Claims.checkForDuplicateProperties(
4443
property: String? = null,
4544
ignoreCase: Boolean = false,
4645
message: String = "The claim collection contains duplicate keys."
4746
) = check(!containsDuplicateProperties(property, ignoreCase)) { message }
47+
48+
/**
49+
* Casts an [AbstractClaim] to the specified type.
50+
*
51+
* @param T The underlying [AbstractClaim] type to cast to.
52+
* @param type The [AbstractClaim] type to cast to.
53+
* @return Returns an instance of [T] cast from this [AbstractClaim].
54+
* @throws ClassCastException if this instance cannot be cast to the specified type [T].
55+
*/
56+
fun <T : AbstractClaim<*>> AbstractClaim<*>.cast(type: Class<T>): T {
57+
return type.cast(this)
58+
}
59+
60+
/**
61+
* Casts an [AbstractClaim] to the specified type.
62+
*
63+
* @param T The underlying [AbstractClaim] type to cast to.
64+
* @return Returns an instance of [T] cast from this [AbstractClaim].
65+
* @throws ClassCastException if this instance cannot be cast to the specified type [T].
66+
*/
67+
inline fun <reified T : AbstractClaim<*>> AbstractClaim<*>.cast(): T {
68+
return cast(T::class.java)
69+
}
70+
71+
/**
72+
* Casts an [Iterable] of [AbstractClaim] to the specified type.
73+
*
74+
* @param T The underlying [AbstractClaim] type to cast to.
75+
* @param type The [AbstractClaim] type to cast to.
76+
* @return Returns an [Iterable] of [T] cast from this [Iterable] of [AbstractClaim].
77+
* @throws ClassCastException if any instance cannot be cast to the specified type [T].
78+
*/
79+
fun <T : AbstractClaim<*>> Claims.cast(type: Class<T>): List<T> {
80+
return map { it.cast(type) }
81+
}
82+
83+
/**
84+
* Casts an [Iterable] of [AbstractClaim] to the specified type.
85+
*
86+
* @param T The underlying [AbstractClaim] type to cast to.
87+
* @return Returns an [Iterable] of [T] cast from this [Iterable] of [AbstractClaim].
88+
* @throws ClassCastException if any instance cannot be cast to the specified type [T].
89+
*/
90+
inline fun <reified T : AbstractClaim<*>> Claims.cast(): List<T> {
91+
return cast(T::class.java)
92+
}
93+
94+
/**
95+
* Filters an [Iterable] of [AbstractClaim] by the specified claim type, and optionally by the value type.
96+
*
97+
* @param T The underlying claim value type.
98+
* @param U The underlying claim type.
99+
* @param claimType The claim type to filter by.
100+
* @param valueType The claim value type to filter by.
101+
* @return Returns a [List] of [U] specified by the claim type, and optionally by the value type.
102+
*/
103+
fun <T, U : AbstractClaim<in T>> Claims.filterByType(claimType: Class<U>, valueType: Class<in T>? = null): List<U> {
104+
return filter { it.javaClass == claimType }
105+
.filterIsInstance(claimType)
106+
.filter { claim -> valueType?.let { claim.value?.javaClass == it } ?: true }
107+
}
108+
109+
/**
110+
* Filters an [Iterable] of [AbstractClaim] by the specified claim type.
111+
*
112+
* @param T The underlying claim type.
113+
* @return Returns a [List] of [T] specified by the claim type.
114+
*/
115+
inline fun <reified T : AbstractClaim<*>> Claims.filterByType(): List<T> {
116+
val claimTypeInfo = object : ClaimTypeInfo<T>() {}
117+
return filterByType(claimTypeInfo.claimType, claimTypeInfo.claimValueType)
118+
}
119+
120+
/**
121+
* Filters an [Iterable] of [AbstractClaim] by the specified property.
122+
*
123+
* @param T The underlying claim type.
124+
* @param property The property to filter by.
125+
* @param ignoreCase Determines whether to ignore the property case when filtering.
126+
* @return Returns an [Iterable] of [AbstractClaim] by the specified property.
127+
*/
128+
fun <T : AbstractClaim<*>> Iterable<T>.filterByProperty(property: String, ignoreCase: Boolean = false): List<T> {
129+
return filter { it.property.equals(property, ignoreCase) }
130+
}

onixlabs-corda-identity-framework-contract/src/main/kotlin/io/onixlabs/corda/identityframework/contract/Extensions.CordaClaim.kt

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,25 @@ import net.corda.core.contracts.StateAndRef
2626
* @param value The amended claim value.
2727
* @return Returns an amended claim.
2828
* @throws IllegalStateException if the amend function of the specified state type cannot be cast to [U].
29-
*
29+
*
3030
* Note that this function tends to fail if you don't override the amend function of custom claim types.
3131
*/
32-
inline fun <T : Any, reified U : CordaClaim<T>> StateAndRef<U>.amend(value: T): U = try {
33-
U::class.java.cast(state.data.amend(ref, value))
34-
} catch (ex: ClassCastException) {
35-
val message = "${ex.message}. Did you forget to override ${U::class.java.simpleName}.amend?"
36-
throw IllegalStateException(message, ex)
32+
inline fun <T : Any, reified U : CordaClaim<T>> StateAndRef<U>.amend(value: T): U {
33+
val amendedClaim = state.data.amend(ref, value)
34+
35+
return try {
36+
U::class.java.cast(amendedClaim)
37+
} catch (classCastException: ClassCastException) {
38+
val baseTypeName = CordaClaim::class.java.canonicalName
39+
val derivedTypeName = U::class.java.canonicalName
40+
val returnTypeName = amendedClaim.javaClass.canonicalName
41+
42+
throw IllegalStateException(buildString {
43+
append("${classCastException.message}. ")
44+
append("This typically occurs if a derived type of '$baseTypeName' does not override 'amend'. ")
45+
append("The 'amend' function of '$derivedTypeName' appears to return '$returnTypeName' instead of '$derivedTypeName'.")
46+
}, classCastException)
47+
}
3748
}
3849

3950
/**

onixlabs-corda-identity-framework-integration/src/test/kotlin/io/onixlabs/corda/identityframework/integration/AttestationIntegrationTests.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616

1717
package io.onixlabs.corda.identityframework.integration
1818

19-
import io.onixlabs.corda.core.services.equalTo
2019
import io.onixlabs.corda.core.services.singleOrNull
2120
import io.onixlabs.corda.core.services.vaultServiceFor
2221
import io.onixlabs.corda.identityframework.contract.Attestation
23-
import io.onixlabs.corda.identityframework.contract.AttestationSchema
2422
import io.onixlabs.corda.identityframework.contract.AttestationStatus
2523
import io.onixlabs.corda.identityframework.contract.CordaClaim
24+
import io.onixlabs.corda.identityframework.workflow.attestationPointer
2625
import net.corda.core.utilities.getOrThrow
2726
import org.junit.jupiter.api.Test
2827
import org.junit.jupiter.api.fail
@@ -52,7 +51,7 @@ class AttestationIntegrationTests : IntegrationTest() {
5251

5352
// Find the issued attestation
5453
val issuedAttestation = nodeC.rpc.vaultServiceFor<Attestation<CordaClaim<String>>>().singleOrNull {
55-
expression(AttestationSchema.AttestationEntity::pointer equalTo issuedClaim.ref.toString())
54+
attestationPointer(issuedClaim.ref)
5655
} ?: fail("Failed to find issued attestation.")
5756

5857
// Amend the issued attestation
@@ -64,7 +63,7 @@ class AttestationIntegrationTests : IntegrationTest() {
6463

6564
// Find the amended attestation
6665
val amendedAttestation = nodeC.rpc.vaultServiceFor<Attestation<CordaClaim<String>>>().singleOrNull {
67-
expression(AttestationSchema.AttestationEntity::pointer equalTo issuedClaim.ref.toString())
66+
attestationPointer(issuedClaim.ref)
6867
} ?: fail("Failed to find amended attestation.")
6968

7069
// Publish the amended attestation
@@ -77,7 +76,7 @@ class AttestationIntegrationTests : IntegrationTest() {
7776
listOf(nodeA, nodeB, nodeC).forEach {
7877
it.waitForTransaction(tx.id)
7978
it.rpc.vaultServiceFor<Attestation<CordaClaim<String>>>().singleOrNull {
80-
expression(AttestationSchema.AttestationEntity::pointer equalTo issuedClaim.ref.toString())
79+
attestationPointer(issuedClaim.ref)
8180
} ?: fail("Failed to find published attestation.")
8281
}
8382

onixlabs-corda-identity-framework-workflow/src/main/kotlin/io/onixlabs/corda/identityframework/workflow/Extensions.FlowLogic.kt

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,11 @@ package io.onixlabs.corda.identityframework.workflow
1818

1919
import co.paralleluniverse.fibers.Suspendable
2020
import io.onixlabs.corda.core.services.any
21-
import io.onixlabs.corda.core.services.equalTo
2221
import io.onixlabs.corda.core.services.vaultServiceFor
23-
import io.onixlabs.corda.core.workflow.currentStep
2422
import io.onixlabs.corda.identityframework.contract.Attestation
25-
import io.onixlabs.corda.identityframework.contract.AttestationSchema
2623
import io.onixlabs.corda.identityframework.contract.CordaClaim
27-
import io.onixlabs.corda.identityframework.contract.CordaClaimSchema
28-
import net.corda.core.flows.*
29-
import net.corda.core.identity.Party
30-
import net.corda.core.transactions.SignedTransaction
31-
import net.corda.core.transactions.TransactionBuilder
32-
import java.security.PublicKey
24+
import net.corda.core.flows.FlowException
25+
import net.corda.core.flows.FlowLogic
3326

3427
/**
3528
* Checks whether the state for the specified attestation has been witnessed by this node.
@@ -54,7 +47,9 @@ fun FlowLogic<*>.checkHasAttestedStateBeenWitnessed(attestation: Attestation<*>)
5447
@Suspendable
5548
fun FlowLogic<*>.checkClaimExists(claim: CordaClaim<*>) {
5649
val claimExists = serviceHub.vaultServiceFor(claim.javaClass).any {
57-
expression(CordaClaimSchema.CordaClaimEntity::hash equalTo claim.hash.toString())
50+
claimType(claim.javaClass)
51+
claimValueType(claim.value.javaClass)
52+
claimHash(claim.hash)
5853
}
5954

6055
if (claimExists) {
@@ -71,7 +66,9 @@ fun FlowLogic<*>.checkClaimExists(claim: CordaClaim<*>) {
7166
@Suspendable
7267
fun FlowLogic<*>.checkAttestationExists(attestation: Attestation<*>) {
7368
val attestationExists = serviceHub.vaultServiceFor(attestation.javaClass).any {
74-
expression(AttestationSchema.AttestationEntity::hash equalTo attestation.hash.toString())
69+
attestationType(attestation.javaClass)
70+
attestationPointerType(attestation.pointer.stateType)
71+
attestationHash(attestation.hash)
7572
}
7673

7774
if (attestationExists) {

0 commit comments

Comments
 (0)