mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Vault Query Service JPA implementation (#840)
* Vault Query Service API implementation using JPA Hibernate Added queryBy(QueryCriteria) Vault API and Junit tests. Minor cosmetic API changes following rebase. Fixes following rebase from master Upgraded to requery 1.3.1 WIP - removed 'latestOnly' from LinearStateQueryCriteria WIP - CommercialSchemas V2, V3, V4 testing WIP - sort out generics handling. WIP - most general queries completed. WIP - join queries, contractStateType derivation WIP - refactoring Requery WIP - refactored VaultService to extract a VaultQueryService interface (and associated Requery implementation). WIP - HibernateVaultQuery implementation WIP - Re-structured all Schema definitions (requery/jpa) and make Hibernate Config reusable. WIP - Multi-version schema testing, hibernate query testing. WIP - Custom Criteria and Fungible Criteria impl & testing. WIP - Kotlin Comparable Generics error WIP - Party queries all working now WIP - All VaultQueryTests now working (refactored for AND / OR composition) WIP - added schema registration in CordaPluginRegistry to enable custom vault queries on arbitrary schemas. WIP - added new default Sort NULL order to be NONE + added lots more tests for Logical Operator testing. Mostly identity fixes following rebase from master. Exception handling and public API cleanup in prep for PR. Additional tests for Logical Operators; additional tests for NULLS sort ordering; additional logging; Additional parser to handle Nullable attribute values; added Unary and Collection logical expression handlers Lots of cleanup: participants; trackBy interfaces; additional fungible tests; parser cleanup and improved support for Java Removed all traces of Requery implementation. Further minor cleanup and Junit test fix. Final identity and schema related identity clean-up. Revert unrelated changes. PR review updates: blank lines, isRelevant. Fixed wiring of updatesPublisher for dynamic trackBy queries. PR review changes: multi-versioned schema samples and associated dummy contracts moved to test packages. Fixed problem with sorted queries (not specifying any filterable criteria). PR review: minor updates to address RP comments. Typesafe custom query criteria Cleanup: remove redundant tests. Further clean-up and make all Java test work successfully. Remove debugging print statements. Rebased from master - changes required due to DealState module change. fixed broken assertion caused by DealState ordering change (different package) Fixed transaction demarcation issue causing "java.lang.IllegalStateException: Was not expecting to find existing database transaction on current strand" trackBy() now filters on ContractType and StateStatus (CONSUMED, UNCONSUMED, ALL) Added tests to exercise RPCOps trackBy and queryBy (RPC smoke test and CordaRPCOps) Added additional @CordaSerializable annotations. Updated documentation and referenced sample code. Added deprecation annotations. Re-added missing deprecation annotation. Hibernate debug logging is now configurable and disabled by default. Introduced common Sort attributes based on the node schemas. Completely removed NULL_HANDLING sort parameter as this is not supported in JPA. Revisited and fixed usage of @CordaSerializable. * Minor fix following rebase from master. * Remove blank line as per RP PR feedback request. * Minor Java documentation and example clean-up. * Disable BFT Notary Service tests.
This commit is contained in:
parent
4d66e21f32
commit
f8ad5c9d10
@ -44,7 +44,7 @@ buildscript {
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
ext.h2_version = '1.4.194'
|
||||
ext.rxjava_version = '1.2.4'
|
||||
ext.requery_version = '1.2.1'
|
||||
ext.requery_version = '1.3.1'
|
||||
ext.dokka_version = '0.9.14'
|
||||
ext.eddsa_version = '0.2.0'
|
||||
|
||||
|
@ -4,6 +4,7 @@ import com.google.common.hash.Hashing
|
||||
import com.google.common.hash.HashingInputStream
|
||||
import net.corda.client.rpc.CordaRPCConnection
|
||||
import net.corda.client.rpc.notUsed
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.POUNDS
|
||||
import net.corda.core.contracts.SWISS_FRANCS
|
||||
@ -14,12 +15,18 @@ import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.startTrackedFlow
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.node.services.vault.SortAttribute
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.sizedInputStreamAndHash
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.smoketesting.NodeConfig
|
||||
import net.corda.smoketesting.NodeProcess
|
||||
@ -156,6 +163,54 @@ class StandaloneCordaRPClientTest {
|
||||
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test vault track by`() {
|
||||
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>()
|
||||
assertEquals(0, vault.totalStatesAvailable)
|
||||
|
||||
var updateCount = 0
|
||||
vaultUpdates.subscribe { update ->
|
||||
log.info("Vault>> FlowId=${update.flowId}")
|
||||
++updateCount
|
||||
}
|
||||
|
||||
// Now issue some cash
|
||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
assertNotEquals(0, updateCount)
|
||||
|
||||
// Check that this cash exists in the vault
|
||||
val cashBalance = rpcProxy.getCashBalances()
|
||||
log.info("Cash Balances: $cashBalance")
|
||||
assertEquals(1, cashBalance.size)
|
||||
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test vault query by`() {
|
||||
// Now issue some cash
|
||||
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
|
||||
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
|
||||
val paging = PageSpecification(0, 10)
|
||||
val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.RECORDED_TIME), Sort.Direction.DESC)))
|
||||
|
||||
val queryResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
|
||||
assertEquals(1, queryResults.totalStatesAvailable)
|
||||
assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity)
|
||||
|
||||
rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryIdentity).returnValue.getOrThrow()
|
||||
|
||||
val moreResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
|
||||
assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100
|
||||
|
||||
// Check that this cash exists in the vault
|
||||
val cashBalance = rpcProxy.getCashBalances()
|
||||
log.info("Cash Balances: $cashBalance")
|
||||
assertEquals(1, cashBalance.size)
|
||||
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
|
||||
}
|
||||
|
||||
private fun fetchNotaryIdentity(): Party {
|
||||
val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates()
|
||||
|
@ -1,4 +1,5 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-jpa'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
|
@ -0,0 +1,60 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.contracts.clauses.FilterOn
|
||||
import net.corda.core.contracts.clauses.verifyClause
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.*
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class DummyLinearContract : Contract {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("Test")
|
||||
|
||||
val clause: Clause<State, CommandData, Unit> = LinearState.ClauseVerifier()
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx,
|
||||
FilterOn(clause, { states -> states.filterIsInstance<State>() }),
|
||||
emptyList())
|
||||
|
||||
data class State(
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(),
|
||||
override val contract: Contract = DummyLinearContract(),
|
||||
override val participants: List<AbstractParty> = listOf(),
|
||||
val linearString: String = "ABC",
|
||||
val linearNumber: Long = 123L,
|
||||
val linearTimestamp: Instant = LocalDateTime.now().toInstant(ZoneOffset.UTC),
|
||||
val linearBoolean: Boolean = true,
|
||||
val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState, QueryableState {
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return participants.any { it.owningKey.containsAny(ourKeys) }
|
||||
}
|
||||
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(DummyLinearStateSchemaV1, DummyLinearStateSchemaV2)
|
||||
|
||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||
return when (schema) {
|
||||
is DummyLinearStateSchemaV1 -> DummyLinearStateSchemaV1.PersistentDummyLinearState(
|
||||
externalId = linearId.externalId,
|
||||
uuid = linearId.id,
|
||||
linearString = linearString,
|
||||
linearNumber = linearNumber,
|
||||
linearTimestamp = linearTimestamp,
|
||||
linearBoolean = linearBoolean
|
||||
)
|
||||
is DummyLinearStateSchemaV2 -> DummyLinearStateSchemaV2.PersistentDummyLinearState(
|
||||
uid = linearId,
|
||||
linearString = linearString,
|
||||
linearNumber = linearNumber,
|
||||
linearTimestamp = linearTimestamp,
|
||||
linearBoolean = linearBoolean
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -111,12 +111,12 @@ interface CordaRPCOps : RPCOps {
|
||||
// Java Helpers
|
||||
|
||||
// DOCSTART VaultQueryAPIJavaHelpers
|
||||
fun <T : ContractState> vaultQueryByCriteria(criteria: QueryCriteria): Vault.Page<T> = vaultQueryBy(criteria = criteria)
|
||||
fun <T : ContractState> vaultQueryByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> = vaultQueryBy(criteria, paging = paging)
|
||||
fun <T : ContractState> vaultQueryByCriteria(criteria: QueryCriteria): Vault.Page<T> = vaultQueryBy(criteria)
|
||||
fun <T : ContractState> vaultQueryByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> = vaultQueryBy(criteria, paging)
|
||||
fun <T : ContractState> vaultQueryByWithSorting(criteria: QueryCriteria, sorting: Sort): Vault.Page<T> = vaultQueryBy(criteria, sorting = sorting)
|
||||
|
||||
fun <T : ContractState> vaultTrackByCriteria(criteria: QueryCriteria): Vault.PageAndUpdates<T> = vaultTrackBy(criteria = criteria)
|
||||
fun <T : ContractState> vaultTrackByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates<T> = vaultTrackBy(criteria, paging = paging)
|
||||
fun <T : ContractState> vaultTrackByCriteria(criteria: QueryCriteria): Vault.PageAndUpdates<T> = vaultTrackBy(criteria)
|
||||
fun <T : ContractState> vaultTrackByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates<T> = vaultTrackBy(criteria, paging)
|
||||
fun <T : ContractState> vaultTrackByWithSorting(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates<T> = vaultTrackBy(criteria, sorting = sorting)
|
||||
// DOCEND VaultQueryAPIJavaHelpers
|
||||
|
||||
@ -125,7 +125,7 @@ interface CordaRPCOps : RPCOps {
|
||||
*/
|
||||
@RPCReturnsObservables
|
||||
// TODO: Remove this from the interface
|
||||
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("vaultTrackBy(QueryCriteria())"))
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("vaultTrackBy(QueryCriteria())"))
|
||||
fun vaultAndUpdates(): Pair<List<StateAndRef<ContractState>>, Observable<Vault.Update>>
|
||||
|
||||
/**
|
||||
|
@ -1,8 +1,12 @@
|
||||
package net.corda.core.node
|
||||
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationCustomization
|
||||
import java.util.function.Function
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.node.services.VaultQueryService
|
||||
|
||||
/**
|
||||
* Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file
|
||||
@ -46,4 +50,13 @@ abstract class CordaPluginRegistry {
|
||||
* @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future.
|
||||
*/
|
||||
open fun customizeSerialization(custom: SerializationCustomization): Boolean = false
|
||||
|
||||
/**
|
||||
* Optionally, custom schemas to be used for contract state persistence and vault custom querying
|
||||
*
|
||||
* For example, if you implement the [QueryableState] interface on a new [ContractState]
|
||||
* it needs to be registered here if you wish to perform custom queries on schema entity attributes using the
|
||||
* [VaultQueryService] API
|
||||
*/
|
||||
open val requiredSchemas: Set<MappedSchema> get() = emptySet()
|
||||
}
|
@ -38,6 +38,7 @@ interface ServicesForResolution {
|
||||
*/
|
||||
interface ServiceHub : ServicesForResolution {
|
||||
val vaultService: VaultService
|
||||
val vaultQueryService: VaultQueryService
|
||||
val keyManagementService: KeyManagementService
|
||||
override val storageService: StorageService
|
||||
val networkMapCache: NetworkMapCache
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.node.services
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -12,7 +13,6 @@ import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
@ -22,6 +22,7 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.io.InputStream
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
@ -68,6 +69,15 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
/** Checks whether the update contains a state of the specified type. */
|
||||
inline fun <reified T : ContractState> containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T }
|
||||
|
||||
/** Checks whether the update contains a state of the specified type and state status */
|
||||
fun <T : ContractState> containsType(clazz: Class<T>, status: StateStatus) =
|
||||
when(status) {
|
||||
StateStatus.UNCONSUMED -> produced.any { clazz.isAssignableFrom(it.state.data.javaClass) }
|
||||
StateStatus.CONSUMED -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) }
|
||||
else -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) }
|
||||
|| produced.any { clazz.isAssignableFrom(it.state.data.javaClass) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two updates into a single update with the combined inputs and outputs of the two updates but net
|
||||
* any outputs of the left-hand-side (this) that are consumed by the inputs of the right-hand-side (rhs).
|
||||
@ -98,6 +108,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
val NoUpdate = Update(emptySet(), emptySet())
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class StateStatus {
|
||||
UNCONSUMED, CONSUMED, ALL
|
||||
}
|
||||
@ -114,9 +125,10 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Page<out T : ContractState>(val states: List<StateAndRef<T>>,
|
||||
val statesMetadata: List<Vault.StateMetadata>,
|
||||
val statesMetadata: List<StateMetadata>,
|
||||
val pageable: PageSpecification,
|
||||
val totalStatesAvailable: Long)
|
||||
val totalStatesAvailable: Int,
|
||||
val stateTypes: StateStatus)
|
||||
|
||||
@CordaSerializable
|
||||
data class StateMetadata(val ref: StateRef,
|
||||
@ -130,7 +142,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
val lockUpdateTime: Instant?)
|
||||
|
||||
@CordaSerializable
|
||||
data class PageAndUpdates<out T : ContractState> (val current: Vault.Page<T>, val future: Observable<Vault.Update>? = null)
|
||||
data class PageAndUpdates<out T : ContractState> (val current: Vault.Page<T>, val future: Observable<Vault.Update>)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,6 +172,11 @@ interface VaultService {
|
||||
*/
|
||||
val updates: Observable<Vault.Update>
|
||||
|
||||
/**
|
||||
* Enable creation of observables of updates.
|
||||
*/
|
||||
val updatesPublisher: PublishSubject<Vault.Update>
|
||||
|
||||
/**
|
||||
* Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for
|
||||
* which we have no cash evaluate to null (not present in map), not 0.
|
||||
@ -171,57 +188,14 @@ interface VaultService {
|
||||
* first subscriber is registered so as to avoid racing with early updates.
|
||||
*/
|
||||
// TODO: Remove this from the interface
|
||||
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("trackBy(QueryCriteria())"))
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("trackBy(QueryCriteria())"))
|
||||
fun track(): Pair<Vault<ContractState>, Observable<Vault.Update>>
|
||||
|
||||
// DOCSTART VaultQueryAPI
|
||||
/**
|
||||
* Generic vault query function which takes a [QueryCriteria] object to define filters,
|
||||
* optional [PageSpecification] and optional [Sort] modification criteria (default unsorted),
|
||||
* and returns a [Vault.Page] object containing the following:
|
||||
* 1. states as a List of <StateAndRef> (page number and size defined by [PageSpecification])
|
||||
* 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table.
|
||||
* 3. the [PageSpecification] used in the query
|
||||
* 4. a total number of results available (for subsequent paging if necessary)
|
||||
*
|
||||
* Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries.
|
||||
* It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification].
|
||||
*/
|
||||
fun <T : ContractState> queryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
|
||||
paging: PageSpecification = PageSpecification(),
|
||||
sorting: Sort = Sort(emptySet())): Vault.Page<T>
|
||||
/**
|
||||
* Generic vault query function which takes a [QueryCriteria] object to define filters,
|
||||
* optional [PageSpecification] and optional [Sort] modification criteria (default unsorted),
|
||||
* and returns a [Vault.PageAndUpdates] object containing
|
||||
* 1) a snapshot as a [Vault.Page] (described previously in [queryBy])
|
||||
* 2) an [Observable] of [Vault.Update]
|
||||
*
|
||||
* Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function.
|
||||
* the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
|
||||
*/
|
||||
fun <T : ContractState> trackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
|
||||
paging: PageSpecification = PageSpecification(),
|
||||
sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates<T>
|
||||
// DOCEND VaultQueryAPI
|
||||
|
||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||
// Java Helpers
|
||||
fun <T : ContractState> queryBy(): Vault.Page<T> = queryBy()
|
||||
fun <T : ContractState> queryBy(criteria: QueryCriteria): Vault.Page<T> = queryBy(criteria)
|
||||
fun <T : ContractState> queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> = queryBy(criteria, paging)
|
||||
fun <T : ContractState> queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page<T> = queryBy(criteria, sorting)
|
||||
|
||||
fun <T : ContractState> trackBy(): Vault.PageAndUpdates<T> = trackBy()
|
||||
fun <T : ContractState> trackBy(criteria: QueryCriteria): Vault.PageAndUpdates<T> = trackBy(criteria)
|
||||
fun <T : ContractState> trackBy(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates<T> = trackBy(criteria, paging)
|
||||
fun <T : ContractState> trackBy(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates<T> = trackBy(criteria, Sort(emptySet()))
|
||||
|
||||
/**
|
||||
* Return unconsumed [ContractState]s for a given set of [StateRef]s
|
||||
*/
|
||||
// TODO: Remove this from the interface
|
||||
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(stateRefs = listOf(<StateRef>)))"))
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(stateRefs = listOf(<StateRef>)))"))
|
||||
fun statesForRefs(refs: List<StateRef>): Map<StateRef, TransactionState<*>?>
|
||||
|
||||
/**
|
||||
@ -300,7 +274,7 @@ interface VaultService {
|
||||
* Optionally may specify whether to include [StateRef] that have been marked as soft locked (default is true)
|
||||
*/
|
||||
// TODO: Remove this from the interface
|
||||
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())"))
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())"))
|
||||
fun <T : ContractState> states(clazzes: Set<Class<T>>, statuses: EnumSet<Vault.StateStatus>, includeSoftLockedStates: Boolean = true): Iterable<StateAndRef<T>>
|
||||
// DOCEND VaultStatesQuery
|
||||
|
||||
@ -347,18 +321,18 @@ interface VaultService {
|
||||
}
|
||||
|
||||
// TODO: Remove this from the interface
|
||||
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria())"))
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria())"))
|
||||
inline fun <reified T : ContractState> VaultService.unconsumedStates(includeSoftLockedStates: Boolean = true): Iterable<StateAndRef<T>> =
|
||||
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED), includeSoftLockedStates)
|
||||
|
||||
// TODO: Remove this from the interface
|
||||
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED))"))
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED))"))
|
||||
inline fun <reified T : ContractState> VaultService.consumedStates(): Iterable<StateAndRef<T>> =
|
||||
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.CONSUMED))
|
||||
|
||||
/** Returns the [linearState] heads only when the type of the state would be considered an 'instanceof' the given type. */
|
||||
// TODO: Remove this from the interface
|
||||
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(linearId = listOf(<UniqueIdentifier>)))"))
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(linearId = listOf(<UniqueIdentifier>)))"))
|
||||
inline fun <reified T : LinearState> VaultService.linearHeadsOfType() =
|
||||
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED))
|
||||
.associateBy { it.state.data.linearId }.mapValues { it.value }
|
||||
@ -367,6 +341,106 @@ class StatesNotAvailableException(override val message: String?, override val ca
|
||||
override fun toString() = "Soft locking error: $message"
|
||||
}
|
||||
|
||||
interface VaultQueryService {
|
||||
|
||||
// DOCSTART VaultQueryAPI
|
||||
/**
|
||||
* Generic vault query function which takes a [QueryCriteria] object to define filters,
|
||||
* optional [PageSpecification] and optional [Sort] modification criteria (default unsorted),
|
||||
* and returns a [Vault.Page] object containing the following:
|
||||
* 1. states as a List of <StateAndRef> (page number and size defined by [PageSpecification])
|
||||
* 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table.
|
||||
* 3. the [PageSpecification] used in the query
|
||||
* 4. a total number of results available (for subsequent paging if necessary)
|
||||
*
|
||||
* @throws VaultQueryException if the query cannot be executed for any reason
|
||||
* (missing criteria or parsing error, invalid operator, unsupported query, underlying database error)
|
||||
*
|
||||
* Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries.
|
||||
* It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification].
|
||||
* Note2: you can also annotate entity fields with JPA OrderBy annotation to achieve the same effect as explicit sorting
|
||||
*/
|
||||
@Throws(VaultQueryException::class)
|
||||
fun <T : ContractState> _queryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
|
||||
paging: PageSpecification = PageSpecification(),
|
||||
sorting: Sort = Sort(emptySet()),
|
||||
contractType: Class<out ContractState>): Vault.Page<T>
|
||||
/**
|
||||
* Generic vault query function which takes a [QueryCriteria] object to define filters,
|
||||
* optional [PageSpecification] and optional [Sort] modification criteria (default unsorted),
|
||||
* and returns a [Vault.PageAndUpdates] object containing
|
||||
* 1) a snapshot as a [Vault.Page] (described previously in [queryBy])
|
||||
* 2) an [Observable] of [Vault.Update]
|
||||
*
|
||||
* @throws VaultQueryException if the query cannot be executed for any reason
|
||||
*
|
||||
* Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function.
|
||||
* the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
|
||||
*/
|
||||
@Throws(VaultQueryException::class)
|
||||
fun <T : ContractState> _trackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
|
||||
paging: PageSpecification = PageSpecification(),
|
||||
sorting: Sort = Sort(emptySet()),
|
||||
contractType: Class<out ContractState>): Vault.PageAndUpdates<T>
|
||||
// DOCEND VaultQueryAPI
|
||||
|
||||
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
|
||||
// Java Helpers
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>): Vault.Page<T> = _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria): Vault.Page<T> = _queryBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> = _queryBy(criteria, paging, Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> = _queryBy(criteria, PageSpecification(), sorting, contractType)
|
||||
fun <T : ContractState> queryBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page<T> = _queryBy(criteria, paging, sorting, contractType)
|
||||
|
||||
fun <T : ContractState> trackBy(contractType: Class<out T>): Vault.Page<T> = _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria): Vault.PageAndUpdates<T> = _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates<T> = _trackBy(criteria, paging, Sort(emptySet()), contractType)
|
||||
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates<T> = _trackBy(criteria, PageSpecification(), sorting, contractType)
|
||||
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates<T> = _trackBy(criteria, paging, sorting, contractType)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.queryBy(): Vault.Page<T> {
|
||||
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.queryBy(criteria: QueryCriteria): Vault.Page<T> {
|
||||
return _queryBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||
return _queryBy(criteria, paging, Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
||||
return _queryBy(criteria, PageSpecification(), sorting, T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page<T> {
|
||||
return _queryBy(criteria, paging, sorting, T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.trackBy(): Vault.PageAndUpdates<T> {
|
||||
return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria): Vault.PageAndUpdates<T> {
|
||||
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates<T> {
|
||||
return _trackBy(criteria, paging, Sort(emptySet()), T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates<T> {
|
||||
return _trackBy(criteria, PageSpecification(), sorting, T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates<T> {
|
||||
return _trackBy(criteria, paging, sorting, T::class.java)
|
||||
}
|
||||
|
||||
class VaultQueryException(description: String) : FlowException("$description")
|
||||
|
||||
/**
|
||||
* The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example,
|
||||
* call out to a hardware security module that enforces various auditing and frequency-of-use requirements.
|
||||
|
@ -3,14 +3,17 @@ package net.corda.core.node.services.vault
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.QueryCriteria.AndComposition
|
||||
import net.corda.core.node.services.vault.QueryCriteria.OrComposition
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.criteria.Predicate
|
||||
|
||||
/**
|
||||
* Indexing assumptions:
|
||||
@ -18,27 +21,39 @@ import java.util.*
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class QueryCriteria {
|
||||
abstract fun visit(parser: IQueryCriteriaParser): Collection<Predicate>
|
||||
|
||||
@CordaSerializable
|
||||
data class TimeCondition(val type: TimeInstantType, val predicate: ColumnPredicate<Instant>)
|
||||
|
||||
/**
|
||||
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
|
||||
*/
|
||||
data class VaultQueryCriteria @JvmOverloads constructor (
|
||||
val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
val stateRefs: List<StateRef>? = null,
|
||||
val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
val stateRefs: List<StateRef>? = null,
|
||||
val notaryName: List<X500Name>? = null,
|
||||
val includeSoftlockedStates: Boolean? = true,
|
||||
val timeCondition: Logical<TimeInstantType, Array<Instant>>? = null,
|
||||
val participantIdentities: List<X500Name>? = null) : QueryCriteria()
|
||||
val includeSoftlockedStates: Boolean = true,
|
||||
val timeCondition: TimeCondition? = null) : QueryCriteria() {
|
||||
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState]
|
||||
*/
|
||||
data class LinearStateQueryCriteria @JvmOverloads constructor(
|
||||
val participants: List<AbstractParty>? = null,
|
||||
val linearId: List<UniqueIdentifier>? = null,
|
||||
val latestOnly: Boolean? = true,
|
||||
val dealRef: List<String>? = null,
|
||||
val dealPartyName: List<X500Name>? = null) : QueryCriteria()
|
||||
val dealRef: List<String>? = null) : QueryCriteria() {
|
||||
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleState]
|
||||
@ -48,13 +63,16 @@ sealed class QueryCriteria {
|
||||
* [Commodity] as used in [CommodityContract] state
|
||||
*/
|
||||
data class FungibleAssetQueryCriteria @JvmOverloads constructor(
|
||||
val ownerIdentity: List<X500Name>? = null,
|
||||
val quantity: Logical<*,Long>? = null,
|
||||
val tokenType: List<Class<out Any>>? = null,
|
||||
val tokenValue: List<String>? = null,
|
||||
val issuerPartyName: List<X500Name>? = null,
|
||||
val issuerRef: List<OpaqueBytes>? = null,
|
||||
val exitKeyIdentity: List<String>? = null) : QueryCriteria()
|
||||
val participants: List<AbstractParty>? = null,
|
||||
val owner: List<AbstractParty>? = null,
|
||||
val quantity: ColumnPredicate<Long>? = null,
|
||||
val issuerPartyName: List<AbstractParty>? = null,
|
||||
val issuerRef: List<OpaqueBytes>? = null) : QueryCriteria() {
|
||||
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VaultCustomQueryCriteria: provides query by custom attributes defined in a contracts
|
||||
@ -62,16 +80,28 @@ sealed class QueryCriteria {
|
||||
* (see Persistence documentation for more information)
|
||||
*
|
||||
* Params
|
||||
* [indexExpression] refers to a (composable) JPA Query like WHERE expression clauses of the form:
|
||||
* [JPA entityAttributeName] [Operand] [Value]
|
||||
* [expression] refers to a (composable) type safe [CriteriaExpression]
|
||||
*
|
||||
* Refer to [CommercialPaper.State] for a concrete example.
|
||||
*/
|
||||
data class VaultCustomQueryCriteria<L,R>(val indexExpression: Logical<L,R>? = null) : QueryCriteria()
|
||||
data class VaultCustomQueryCriteria<L : PersistentState>(val expression: CriteriaExpression<L, Boolean>) : QueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
// enable composition of [QueryCriteria]
|
||||
data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria()
|
||||
data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria()
|
||||
data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseAnd(this.a, this.b)
|
||||
}
|
||||
}
|
||||
|
||||
data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseOr(this.a, this.b)
|
||||
}
|
||||
}
|
||||
|
||||
// timestamps stored in the vault states table [VaultSchema.VaultStates]
|
||||
@CordaSerializable
|
||||
@ -81,5 +111,15 @@ sealed class QueryCriteria {
|
||||
}
|
||||
}
|
||||
|
||||
interface IQueryCriteriaParser {
|
||||
fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate>
|
||||
fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate>
|
||||
fun <L: PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate>
|
||||
fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection<Predicate>
|
||||
fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection<Predicate>
|
||||
fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate>
|
||||
fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection<Predicate>
|
||||
}
|
||||
|
||||
infix fun QueryCriteria.and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria)
|
||||
infix fun QueryCriteria.or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria)
|
||||
|
@ -1,61 +1,93 @@
|
||||
package net.corda.core.node.services.vault
|
||||
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.lang.reflect.Field
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.jvm.javaField
|
||||
|
||||
@CordaSerializable
|
||||
enum class Operator {
|
||||
enum class BinaryLogicalOperator {
|
||||
AND,
|
||||
OR,
|
||||
OR
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class EqualityComparisonOperator {
|
||||
EQUAL,
|
||||
NOT_EQUAL,
|
||||
NOT_EQUAL
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class BinaryComparisonOperator {
|
||||
LESS_THAN,
|
||||
LESS_THAN_OR_EQUAL,
|
||||
GREATER_THAN,
|
||||
GREATER_THAN_OR_EQUAL,
|
||||
IN,
|
||||
NOT_IN,
|
||||
LIKE,
|
||||
NOT_LIKE,
|
||||
BETWEEN,
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class NullOperator {
|
||||
IS_NULL,
|
||||
NOT_NULL
|
||||
}
|
||||
|
||||
interface Condition<L, R> {
|
||||
val leftOperand: L
|
||||
val operator: Operator
|
||||
val rightOperand: R
|
||||
}
|
||||
|
||||
interface AndOr<out Q> {
|
||||
infix fun <V> and(condition: Condition<V, *>): Q
|
||||
infix fun <V> or(condition: Condition<V, *>): Q
|
||||
@CordaSerializable
|
||||
enum class LikenessOperator {
|
||||
LIKE,
|
||||
NOT_LIKE
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
sealed class Logical<L, R> : Condition<L, R>, AndOr<Logical<*, *>>
|
||||
|
||||
class LogicalExpression<L, R>(leftOperand: L,
|
||||
operator: Operator,
|
||||
rightOperand: R? = null) : Logical<L, R>() {
|
||||
init {
|
||||
if (rightOperand == null) {
|
||||
check(operator in setOf(Operator.NOT_NULL, Operator.IS_NULL),
|
||||
{ "Must use a unary operator (${Operator.IS_NULL}, ${Operator.NOT_NULL}) if right operand is null"} )
|
||||
}
|
||||
else {
|
||||
check(operator !in setOf(Operator.NOT_NULL, Operator.IS_NULL),
|
||||
{ "Cannot use a unary operator (${Operator.IS_NULL}, ${Operator.NOT_NULL}) if right operand is not null"} )
|
||||
}
|
||||
}
|
||||
override fun <V> and(condition: Condition<V, *>): Logical<*, *> = LogicalExpression(this, Operator.AND, condition)
|
||||
override fun <V> or(condition: Condition<V, *>): Logical<*, *> = LogicalExpression(this, Operator.OR, condition)
|
||||
|
||||
override val operator: Operator = operator
|
||||
override val rightOperand: R = rightOperand as R
|
||||
override val leftOperand: L = leftOperand
|
||||
enum class CollectionOperator {
|
||||
IN,
|
||||
NOT_IN
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
sealed class CriteriaExpression<O, out T> {
|
||||
data class BinaryLogical<O>(val left: CriteriaExpression<O, Boolean>, val right: CriteriaExpression<O, Boolean>, val operator: BinaryLogicalOperator) : CriteriaExpression<O, Boolean>()
|
||||
data class Not<O>(val expression: CriteriaExpression<O, Boolean>) : CriteriaExpression<O, Boolean>()
|
||||
data class ColumnPredicateExpression<O, C>(val column: Column<O, C>, val predicate: ColumnPredicate<C>) : CriteriaExpression<O, Boolean>()
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
sealed class Column<O, out C> {
|
||||
data class Java<O, out C>(val field: Field) : Column<O, C>()
|
||||
data class Kotlin<O, out C>(val property: KProperty1<O, C?>) : Column<O, C>()
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
sealed class ColumnPredicate<C> {
|
||||
data class EqualityComparison<C>(val operator: EqualityComparisonOperator, val rightLiteral: C) : ColumnPredicate<C>()
|
||||
data class BinaryComparison<C : Comparable<C>>(val operator: BinaryComparisonOperator, val rightLiteral: C) : ColumnPredicate<C>()
|
||||
data class Likeness(val operator: LikenessOperator, val rightLiteral: String) : ColumnPredicate<String>()
|
||||
data class CollectionExpression<C>(val operator: CollectionOperator, val rightLiteral: Collection<C>) : ColumnPredicate<C>()
|
||||
data class Between<C : Comparable<C>>(val rightFromLiteral: C, val rightToLiteral: C) : ColumnPredicate<C>()
|
||||
data class NullExpression<C>(val operator: NullOperator) : ColumnPredicate<C>()
|
||||
}
|
||||
|
||||
fun <O, R> resolveEnclosingObjectFromExpression(expression: CriteriaExpression<O, R>): Class<O> {
|
||||
return when (expression) {
|
||||
is CriteriaExpression.BinaryLogical -> resolveEnclosingObjectFromExpression(expression.left)
|
||||
is CriteriaExpression.Not -> resolveEnclosingObjectFromExpression(expression.expression)
|
||||
is CriteriaExpression.ColumnPredicateExpression<O, *> -> resolveEnclosingObjectFromColumn(expression.column)
|
||||
}
|
||||
}
|
||||
|
||||
fun <O, C> resolveEnclosingObjectFromColumn(column: Column<O, C>): Class<O> {
|
||||
return when (column) {
|
||||
is Column.Java -> column.field.declaringClass as Class<O>
|
||||
is Column.Kotlin -> column.property.javaField!!.declaringClass as Class<O>
|
||||
}
|
||||
}
|
||||
|
||||
fun <O, C> getColumnName(column: Column<O, C>): String {
|
||||
return when (column) {
|
||||
is Column.Java -> column.field.name
|
||||
is Column.Kotlin -> column.property.name
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination and Ordering
|
||||
@ -70,21 +102,21 @@ class LogicalExpression<L, R>(leftOperand: L,
|
||||
* paging and sorting capability:
|
||||
* https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html
|
||||
*/
|
||||
val DEFAULT_PAGE_NUM = 0L
|
||||
val DEFAULT_PAGE_SIZE = 200L
|
||||
val DEFAULT_PAGE_NUM = 0
|
||||
val DEFAULT_PAGE_SIZE = 200
|
||||
|
||||
/**
|
||||
* Note: this maximum size will be configurable in future (to allow for large JVM heap sized node configurations)
|
||||
* Use [PageSpecification] to correctly handle a number of bounded pages of [MAX_PAGE_SIZE].
|
||||
*/
|
||||
val MAX_PAGE_SIZE = 512L
|
||||
val MAX_PAGE_SIZE = 512
|
||||
|
||||
/**
|
||||
* PageSpecification allows specification of a page number (starting from 0 as default) and page size (defaulting to
|
||||
* [DEFAULT_PAGE_SIZE] with a maximum page size of [DEFAULT_PAGE_SIZE]
|
||||
* [DEFAULT_PAGE_SIZE] with a maximum page size of [MAX_PAGE_SIZE]
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class PageSpecification(val pageNumber: Long = DEFAULT_PAGE_NUM, val pageSize: Long = DEFAULT_PAGE_SIZE)
|
||||
data class PageSpecification(val pageNumber: Int = DEFAULT_PAGE_NUM, val pageSize: Int = DEFAULT_PAGE_SIZE)
|
||||
|
||||
/**
|
||||
* Sort allows specification of a set of entity attribute names and their associated directionality
|
||||
@ -97,17 +129,106 @@ data class Sort(val columns: Collection<SortColumn>) {
|
||||
ASC,
|
||||
DESC
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class NullHandling {
|
||||
NULLS_FIRST,
|
||||
NULLS_LAST
|
||||
}
|
||||
/**
|
||||
* [columnName] should reference a persisted entity attribute name as defined by the associated mapped schema
|
||||
* (for example, [VaultSchema.VaultStates::txId.name])
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class SortColumn(val columnName: String, val direction: Sort.Direction = Sort.Direction.ASC,
|
||||
val nullHandling: Sort.NullHandling = if (direction == Sort.Direction.ASC) Sort.NullHandling.NULLS_LAST else Sort.NullHandling.NULLS_FIRST)
|
||||
interface Attribute
|
||||
|
||||
enum class VaultStateAttribute(val columnName: String) : Attribute {
|
||||
/** Vault States */
|
||||
NOTARY_NAME("notaryName"),
|
||||
CONTRACT_TYPE("contractStateClassName"),
|
||||
STATE_STATUS("stateStatus"),
|
||||
RECORDED_TIME("recordedTime"),
|
||||
CONSUMED_TIME("consumedTime"),
|
||||
LOCK_ID("lockId"),
|
||||
}
|
||||
|
||||
enum class LinearStateAttribute(val columnName: String) : Attribute {
|
||||
/** Vault Linear States */
|
||||
UUID("uuid"),
|
||||
EXTERNAL_ID("externalId"),
|
||||
DEAL_REFERENCE("dealReference"),
|
||||
}
|
||||
|
||||
enum class FungibleStateAttribute(val columnName: String) : Attribute {
|
||||
/** Vault Fungible States */
|
||||
QUANTITY("quantity"),
|
||||
ISSUER_REF("issuerRef")
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class SortColumn(
|
||||
val sortAttribute: SortAttribute,
|
||||
val direction: Sort.Direction = Sort.Direction.ASC)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
sealed class SortAttribute {
|
||||
/**
|
||||
* [sortAttribute] refers to common table attributes defined in the node schemas:
|
||||
* VaultState, VaultLinearStates, VaultFungibleStates
|
||||
*/
|
||||
data class Standard(val attribute: Sort.Attribute) : SortAttribute()
|
||||
|
||||
/**
|
||||
* [entityStateClass] should reference a persistent state entity
|
||||
* [entityStateColumnName] should reference an entity attribute name as defined by the associated mapped schema
|
||||
* (for example, [CashSchemaV1.PersistentCashState::currency.name])
|
||||
*/
|
||||
data class Custom(val entityStateClass: Class<out PersistentState>,
|
||||
val entityStateColumnName: String) : SortAttribute()
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
object Builder {
|
||||
|
||||
fun <R : Comparable<R>> compare(operator: BinaryComparisonOperator, value: R) = ColumnPredicate.BinaryComparison(operator, value)
|
||||
|
||||
fun <O, R> KProperty1<O, R?>.predicate(predicate: ColumnPredicate<R>) = CriteriaExpression.ColumnPredicateExpression(Column.Kotlin(this), predicate)
|
||||
fun <R> Field.predicate(predicate: ColumnPredicate<R>) = CriteriaExpression.ColumnPredicateExpression(Column.Java<Any, R>(this), predicate)
|
||||
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value))
|
||||
fun <R : Comparable<R>> Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value))
|
||||
|
||||
fun <O, R> KProperty1<O, R?>.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value))
|
||||
fun <O, R> KProperty1<O, R?>.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value))
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value)
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value)
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to))
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
|
||||
fun <O, R : Comparable<R>> KProperty1<O, R?>.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
|
||||
|
||||
fun <R> Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value))
|
||||
fun <R> Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value))
|
||||
fun <R : Comparable<R>> Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value)
|
||||
fun <R : Comparable<R>> Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
|
||||
fun <R : Comparable<R>> Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value)
|
||||
fun <R : Comparable<R>> Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
|
||||
fun <R : Comparable<R>> Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to))
|
||||
fun <R : Comparable<R>> Field.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
|
||||
fun <R : Comparable<R>> Field.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
|
||||
|
||||
fun <R> equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)
|
||||
fun <R> notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)
|
||||
fun <R : Comparable<R>> lessThan(value: R) = compare(BinaryComparisonOperator.LESS_THAN, value)
|
||||
fun <R : Comparable<R>> lessThanOrEqual(value: R) = compare(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
|
||||
fun <R : Comparable<R>> greaterThan(value: R) = compare(BinaryComparisonOperator.GREATER_THAN, value)
|
||||
fun <R : Comparable<R>> greaterThanOrEqual(value: R) = compare(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
|
||||
fun <R : Comparable<R>> between(from: R, to: R) = ColumnPredicate.Between(from, to)
|
||||
fun <R : Comparable<R>> `in`(collection: Collection<R>) = ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)
|
||||
fun <R : Comparable<R>> notIn(collection: Collection<R>) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)
|
||||
|
||||
fun <O> KProperty1<O, String?>.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string))
|
||||
fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string))
|
||||
fun <O> KProperty1<O, String?>.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string))
|
||||
fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string))
|
||||
|
||||
fun <O, R> KProperty1<O, R?>.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL))
|
||||
fun Field.isNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.IS_NULL))
|
||||
fun <O, R> KProperty1<O, R?>.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL))
|
||||
fun Field.notNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.NOT_NULL))
|
||||
}
|
||||
|
||||
inline fun <A> builder(block: Builder.() -> A) = block(Builder)
|
||||
|
96
core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt
Normal file
96
core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt
Normal file
@ -0,0 +1,96 @@
|
||||
package net.corda.node.services.vault.schemas.jpa
|
||||
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.StatePersistable
|
||||
import java.util.*
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* JPA representation of the common schema entities
|
||||
*/
|
||||
object CommonSchema
|
||||
|
||||
/**
|
||||
* First version of the Vault ORM schema
|
||||
*/
|
||||
object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = listOf(Party::class.java)) {
|
||||
|
||||
@MappedSuperclass
|
||||
open class LinearState(
|
||||
/**
|
||||
* Represents a [LinearState] [UniqueIdentifier]
|
||||
*/
|
||||
@Column(name = "external_id")
|
||||
var externalId: String?,
|
||||
|
||||
@Column(name = "uuid", nullable = false)
|
||||
var uuid: UUID
|
||||
|
||||
) : PersistentState() {
|
||||
constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id)
|
||||
}
|
||||
|
||||
@MappedSuperclass
|
||||
open class FungibleState(
|
||||
/** [ContractState] attributes */
|
||||
@OneToMany(cascade = arrayOf(CascadeType.ALL))
|
||||
var participants: Set<CommonSchemaV1.Party>,
|
||||
|
||||
/** [OwnableState] attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var ownerKey: CommonSchemaV1.Party,
|
||||
|
||||
/** [FungibleAsset] attributes
|
||||
*
|
||||
* Note: the underlying Product being issued must be modelled into the
|
||||
* custom contract itself (eg. see currency in Cash contract state)
|
||||
*/
|
||||
|
||||
/** Amount attributes */
|
||||
@Column(name = "quantity")
|
||||
var quantity: Long,
|
||||
|
||||
/** Issuer attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var issuerParty: CommonSchemaV1.Party,
|
||||
|
||||
@Column(name = "issuer_reference")
|
||||
var issuerRef: ByteArray
|
||||
) : PersistentState() {
|
||||
constructor(_participants: Set<AbstractParty>, _ownerKey: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: ByteArray)
|
||||
: this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(),
|
||||
ownerKey = CommonSchemaV1.Party(_ownerKey),
|
||||
quantity = _quantity,
|
||||
issuerParty = CommonSchemaV1.Party(_issuerParty),
|
||||
issuerRef = _issuerRef)
|
||||
}
|
||||
|
||||
/**
|
||||
* Party entity (to be replaced by referencing final Identity Schema)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "vault_party",
|
||||
indexes = arrayOf(Index(name = "party_name_idx", columnList = "party_name")))
|
||||
class Party(
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(name = "party_id")
|
||||
var id: Int,
|
||||
|
||||
/**
|
||||
* [Party] attributes
|
||||
*/
|
||||
@Column(name = "party_name")
|
||||
var name: String,
|
||||
|
||||
@Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?)
|
||||
var key: String
|
||||
) {
|
||||
constructor(party: net.corda.core.identity.AbstractParty)
|
||||
: this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String())
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package net.corda.core.schemas
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [DummyDealStateSchema] family name (i.e. independent of version).
|
||||
*/
|
||||
object DummyDealStateSchema
|
||||
|
||||
/**
|
||||
* First version of a cash contract ORM schema that maps all fields of the [DummyDealState] contract state as it stood
|
||||
* at the time of writing.
|
||||
*/
|
||||
object DummyDealStateSchemaV1 : net.corda.core.schemas.MappedSchema(schemaFamily = net.corda.core.schemas.DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(net.corda.core.schemas.DummyDealStateSchemaV1.PersistentDummyDealState::class.java)) {
|
||||
@javax.persistence.Entity
|
||||
@javax.persistence.Table(name = "dummy_deal_states")
|
||||
class PersistentDummyDealState(
|
||||
|
||||
@javax.persistence.Column(name = "deal_reference")
|
||||
var dealReference: String,
|
||||
|
||||
/** parent attributes */
|
||||
@javax.persistence.Transient
|
||||
val uid: net.corda.core.contracts.UniqueIdentifier
|
||||
|
||||
) : net.corda.node.services.vault.schemas.jpa.CommonSchemaV1.LinearState(uid = uid)
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package net.corda.core.schemas
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Index
|
||||
import javax.persistence.Table
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [DummyLinearStateSchema] family name (i.e. independent of version).
|
||||
*/
|
||||
object DummyLinearStateSchema
|
||||
|
||||
/**
|
||||
* First version of a cash contract ORM schema that maps all fields of the [DummyLinearState] contract state as it stood
|
||||
* at the time of writing.
|
||||
*/
|
||||
object DummyLinearStateSchemaV1 : MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyLinearState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "dummy_linear_states",
|
||||
indexes = arrayOf(Index(name = "external_id_idx", columnList = "external_id"),
|
||||
Index(name = "uuid_idx", columnList = "uuid")))
|
||||
class PersistentDummyLinearState(
|
||||
/**
|
||||
* UniqueIdentifier
|
||||
*/
|
||||
@Column(name = "external_id")
|
||||
var externalId: String?,
|
||||
|
||||
@Column(name = "uuid", nullable = false)
|
||||
var uuid: UUID,
|
||||
|
||||
/**
|
||||
* Dummy attributes
|
||||
*/
|
||||
@Column(name = "linear_string")
|
||||
var linearString: String,
|
||||
|
||||
@Column(name = "linear_number")
|
||||
var linearNumber: Long,
|
||||
|
||||
@Column(name = "linear_timestamp")
|
||||
var linearTimestamp: Instant,
|
||||
|
||||
@Column(name = "linear_boolean")
|
||||
var linearBoolean: Boolean
|
||||
) : PersistentState()
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.corda.core.schemas
|
||||
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
|
||||
/**
|
||||
* Second version of a cash contract ORM schema that extends the common
|
||||
* [VaultLinearState] abstract schema
|
||||
*/
|
||||
object DummyLinearStateSchemaV2 : net.corda.core.schemas.MappedSchema(schemaFamily = DummyLinearStateSchema.javaClass, version = 2,
|
||||
mappedTypes = listOf(PersistentDummyLinearState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "dummy_linear_states_v2")
|
||||
class PersistentDummyLinearState(
|
||||
@Column(name = "linear_string") var linearString: String,
|
||||
|
||||
@Column(name = "linear_number") var linearNumber: Long,
|
||||
|
||||
@Column(name = "linear_timestamp") var linearTimestamp: java.time.Instant,
|
||||
|
||||
@Column(name = "linear_boolean") var linearBoolean: Boolean,
|
||||
|
||||
/** parent attributes */
|
||||
@Transient
|
||||
val uid: net.corda.core.contracts.UniqueIdentifier
|
||||
) : CommonSchemaV1.LinearState(uid = uid)
|
||||
}
|
@ -49,7 +49,7 @@ open class MappedSchema(schemaFamily: Class<*>,
|
||||
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
|
||||
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
|
||||
*/
|
||||
@MappedSuperclass open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : Persistable
|
||||
@MappedSuperclass open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : StatePersistable
|
||||
|
||||
/**
|
||||
* Embedded [StateRef] representation used in state mapping.
|
||||
@ -64,3 +64,8 @@ data class PersistentStateRef(
|
||||
) : Serializable {
|
||||
constructor(stateRef: StateRef) : this(stateRef.txhash.bytes.toHexString(), stateRef.index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker interface to denote a persistable Corda state entity that will always have a transaction id and index
|
||||
*/
|
||||
interface StatePersistable : Persistable
|
@ -4,6 +4,7 @@ import io.requery.Key
|
||||
import io.requery.Persistable
|
||||
import io.requery.Superclass
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.schemas.StatePersistable
|
||||
|
||||
import javax.persistence.Column
|
||||
|
||||
@ -14,7 +15,7 @@ object Requery {
|
||||
*/
|
||||
// TODO: this interface will supercede the existing [PersistentState] interface defined in PersistentTypes.kt
|
||||
// once we cut-over all existing Hibernate ContractState persistence to Requery
|
||||
@Superclass interface PersistentState : Persistable {
|
||||
@Superclass interface PersistentState : StatePersistable {
|
||||
@get:Key
|
||||
@get:Column(name = "transaction_id", length = 64)
|
||||
var txId: String
|
||||
|
@ -16,7 +16,7 @@ import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.persistence.schemas.AttachmentEntity
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.services.statemachine.SessionInit
|
||||
import net.corda.node.utilities.transaction
|
||||
import net.corda.testing.node.MockNetwork
|
||||
|
@ -9,7 +9,7 @@ Corda provides a number of flexible query mechanisms for accessing the Vault:
|
||||
- custom JPA_/JPQL_ queries
|
||||
- custom 3rd party Data Access frameworks such as `Spring Data <http://projects.spring.io/spring-data>`_
|
||||
|
||||
The majority of query requirements can be satified by using the Vault Query API, which is exposed via the ``VaultService`` for use directly by flows:
|
||||
The majority of query requirements can be satisfied by using the Vault Query API, which is exposed via the ``VaultQueryService`` for use directly by flows:
|
||||
|
||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/Services.kt
|
||||
:language: kotlin
|
||||
@ -40,51 +40,63 @@ The API provides both static (snapshot) and dynamic (snapshot with streaming upd
|
||||
- Use ``queryBy`` to obtain a only current snapshot of data (for a given ``QueryCriteria``)
|
||||
- Use ``trackBy`` to obtain a both a current snapshot and a future stream of updates (for a given ``QueryCriteria``)
|
||||
|
||||
Simple pagination (page number and size) and sorting (directional ordering, null handling, custom property sort) is also specifiable.
|
||||
Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC, nullHandling = NULLS_LAST).
|
||||
.. note:: Streaming updates are only filtered based on contract type and state status (UNCONSUMED, CONSUMED, ALL)
|
||||
|
||||
The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of logical operators. There are four implementations of this interface which can be chained together to define advanced filters.
|
||||
Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable.
|
||||
Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC).
|
||||
|
||||
The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN).
|
||||
|
||||
There are four implementations of this interface which can be chained together to define advanced filters.
|
||||
|
||||
1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED).
|
||||
|
||||
.. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true).
|
||||
|
||||
2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module).
|
||||
2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s).
|
||||
|
||||
.. note:: Contract states that extend the ``FungibleAsset`` interface now automatically persist associated state attributes.
|
||||
.. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table.
|
||||
|
||||
3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo)
|
||||
3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), dealRef(s).
|
||||
|
||||
.. note:: Contract states that extend the ``LinearState`` or ``DealState`` interfaces now automatically persist associated state attributes.
|
||||
.. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table.
|
||||
|
||||
4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the Persistence_ documentation and associated examples.
|
||||
Custom criteria expressions are expressed as JPA Query like WHERE clauses as follows: [JPA entityAttributeName] [Operand] [Value]
|
||||
4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the api-persistence_ documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression. The ColumnPredicateExpression allows for specification arbitrary criteria using the previously enumerated operator types. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``.
|
||||
|
||||
.. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`)
|
||||
|
||||
An example is illustrated here:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample16
|
||||
:end-before: DOCEND VaultQueryExample16
|
||||
:start-after: DOCSTART VaultQueryExample20
|
||||
:end-before: DOCEND VaultQueryExample20
|
||||
|
||||
All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above.
|
||||
|
||||
Additional notes
|
||||
|
||||
.. note:: Custom contract states that implement the ``Queryable`` interface may now extend the ``FungiblePersistentState``, ``LinearPersistentState`` or ``DealPersistentState`` classes when implementing their ``MappedSchema``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset``, ``LinearState`` and ``DealState`` attributes.
|
||||
.. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples.
|
||||
|
||||
Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java.
|
||||
|
||||
The Vault Query API leverages the rich semantics of the underlying Requery_ persistence framework adopted by Corda.
|
||||
.. note:: When specifying the Contract Type as a parameterised type to the QueryCriteria in Kotlin, queries now include all concrete implementations of that type if this is an interface. Previously, it was only possible to query on Concrete types (or the universe of all Contract States).
|
||||
|
||||
.. _Requery: https://github.com/requery/requery/wiki
|
||||
.. _Persistence: https://docs.corda.net/persistence.html
|
||||
The Vault Query API leverages the rich semantics of the underlying JPA Hibernate_ based Persistence_ framework adopted by Corda.
|
||||
|
||||
.. _Hibernate: https://docs.jboss.org/hibernate/jpa/2.1/api/
|
||||
.. _Persistence: https://docs.corda.net/api-persistence.html
|
||||
|
||||
.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables.
|
||||
|
||||
.. note:: API's now provide ease of use calling semantics from both Java and Kotlin.
|
||||
.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. However, it should be noted that Java custom queries are significantly more verbose due to the use of reflection fields to reference schema attribute types.
|
||||
|
||||
.. note:: Current queries by ``Party`` specify only a party name as the underlying identity framework is being re-designed. In future it may be possible to query on specific elements of a parties identity such as a ``CompositeKey`` hierarchy (parent and child nodes, weightings).
|
||||
An example of a custom query in Java is illustrated here:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample3
|
||||
:end-before: DOCEND VaultJavaQueryExample3
|
||||
|
||||
.. note:: Current queries by ``Party`` specify the ``AbstractParty`` which may be concrete or anonymous. In the later case, where an anonymous party does not have an associated X500Name, then no query results will ever be produced. For performance reasons, queries do not use PublicKey as search criteria. Ongoing design work on identity manangement is likely to enhance identity based queries (including composite key criteria selection).
|
||||
|
||||
Example usage
|
||||
-------------
|
||||
@ -94,7 +106,7 @@ Kotlin
|
||||
|
||||
**General snapshot queries using** ``VaultQueryCriteria``
|
||||
|
||||
Query for all unconsumed states:
|
||||
Query for all unconsumed states (simplest query possible):
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
@ -122,6 +134,8 @@ Query for unconsumed states for a given notary:
|
||||
:start-after: DOCSTART VaultQueryExample4
|
||||
:end-before: DOCEND VaultQueryExample4
|
||||
|
||||
.. note:: We are using the notaries X500Name as our search identifier.
|
||||
|
||||
Query for unconsumed states for a given set of participants:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
@ -136,13 +150,17 @@ Query for unconsumed states recorded between two time intervals:
|
||||
:start-after: DOCSTART VaultQueryExample6
|
||||
:end-before: DOCEND VaultQueryExample6
|
||||
|
||||
Query for all states with pagination specification:
|
||||
.. note:: This example illustrates usage of a Between ColumnPredicate.
|
||||
|
||||
Query for all states with pagination specification (10 results per page):
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample7
|
||||
:end-before: DOCEND VaultQueryExample7
|
||||
|
||||
.. note:: The result set metadata field `totalStatesAvailable` allows you to further paginate accordingly.
|
||||
|
||||
**LinearState and DealState queries using** ``LinearStateQueryCriteria``
|
||||
|
||||
Query for unconsumed linear states for given linear ids:
|
||||
@ -152,7 +170,7 @@ Query for unconsumed linear states for given linear ids:
|
||||
:start-after: DOCSTART VaultQueryExample8
|
||||
:end-before: DOCEND VaultQueryExample8
|
||||
|
||||
.. note:: This example was previously executed using the deprecated extension method:
|
||||
This example was previously executed using the deprecated extension method:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
@ -166,7 +184,7 @@ Query for all linear states associated with a linear id:
|
||||
:start-after: DOCSTART VaultQueryExample9
|
||||
:end-before: DOCEND VaultQueryExample9
|
||||
|
||||
.. note:: This example was previously executed using the deprecated method:
|
||||
This example was previously executed using the deprecated method:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
@ -196,13 +214,15 @@ Query for fungible assets for a given currency:
|
||||
:start-after: DOCSTART VaultQueryExample12
|
||||
:end-before: DOCEND VaultQueryExample12
|
||||
|
||||
Query for fungible assets for a given currency and minimum quantity:
|
||||
Query for fungible assets for a minimum quantity:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample13
|
||||
:end-before: DOCEND VaultQueryExample13
|
||||
|
||||
.. note:: This example uses the builder DSL.
|
||||
|
||||
Query for fungible assets for a specifc issuer party:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
@ -210,45 +230,84 @@ Query for fungible assets for a specifc issuer party:
|
||||
:start-after: DOCSTART VaultQueryExample14
|
||||
:end-before: DOCEND VaultQueryExample14
|
||||
|
||||
Query for consumed fungible assets with a specific exit key:
|
||||
**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable<Vault.Update>``. Refer to `ReactiveX Observable <http://reactivex.io/documentation/observable.html>`_ for a detailed understanding and usage of this type.
|
||||
|
||||
Track unconsumed cash states:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample15
|
||||
:end-before: DOCEND VaultQueryExample15
|
||||
|
||||
Track unconsumed linear states:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample16
|
||||
:end-before: DOCEND VaultQueryExample16
|
||||
|
||||
.. note:: This will return both Deal and Linear states.
|
||||
|
||||
Track unconsumed deal states:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART VaultQueryExample17
|
||||
:end-before: DOCEND VaultQueryExample17
|
||||
|
||||
.. note:: This will return only Deal states.
|
||||
|
||||
Java examples
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
Query for all consumed contract states:
|
||||
Query for all unconsumed linear states:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample0
|
||||
:end-before: DOCEND VaultJavaQueryExample0
|
||||
|
||||
This example was previously executed using the deprecated method:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultDeprecatedJavaQueryExample0
|
||||
:end-before: DOCEND VaultDeprecatedJavaQueryExample0
|
||||
|
||||
Query for all consumed cash states:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample1
|
||||
:end-before: DOCEND VaultJavaQueryExample1
|
||||
|
||||
.. note:: This example was previously executed using the deprecated method:
|
||||
This example was previously executed using the deprecated method:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultDeprecatedJavaQueryExample1
|
||||
:end-before: DOCEND VaultDeprecatedJavaQueryExample1
|
||||
|
||||
Query for all deal states:
|
||||
Query for consumed deal states or linear ids, specify a paging specification and sort by unique identifier:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample2
|
||||
:end-before: DOCEND VaultJavaQueryExample2
|
||||
|
||||
.. note:: This example was previously executed using the deprecated method:
|
||||
Track unconsumed cash states:
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultDeprecatedJavaQueryExample2
|
||||
:end-before: DOCEND VaultDeprecatedJavaQueryExample2
|
||||
:start-after: DOCSTART VaultJavaQueryExample4
|
||||
:end-before: DOCEND VaultJavaQueryExample4
|
||||
|
||||
**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable<Vault.Update>``. Refer to `ReactiveX Observable <http://reactivex.io/documentation/observable.html>`_ for a detailed understanding and usage of this type.
|
||||
Track unconsumed deal states or linear states (with snapshot including specification of paging and sorting by unique identifier):
|
||||
|
||||
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java
|
||||
:language: java
|
||||
:start-after: DOCSTART VaultJavaQueryExample4
|
||||
:end-before: DOCEND VaultJavaQueryExample4
|
||||
|
||||
Other use case scenarios
|
||||
------------------------
|
||||
|
@ -30,6 +30,19 @@ configurations.testCompile {
|
||||
exclude group: 'javassist', module: 'javassist'
|
||||
}
|
||||
|
||||
configurations {
|
||||
testArtifacts.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
task testJar(type: Jar) {
|
||||
classifier "tests"
|
||||
from sourceSets.test.output
|
||||
}
|
||||
|
||||
artifacts {
|
||||
testArtifacts testJar
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'corda-finance'
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ class CommercialPaper : Contract {
|
||||
// DOCSTART VaultIndexedQueryCriteria
|
||||
/** Object Relational Mapping support. */
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(CommercialPaperSchemaV1)
|
||||
/** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */
|
||||
|
||||
/** Object Relational Mapping support. */
|
||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||
@ -98,6 +99,7 @@ class CommercialPaper : Contract {
|
||||
faceValueIssuerParty = this.faceValue.token.issuer.party.owningKey.toBase58String(),
|
||||
faceValueIssuerRef = this.faceValue.token.issuer.reference.bytes
|
||||
)
|
||||
/** Additional schema mappings would be added here (eg. CommercialPaperV2, ...) */
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
package net.corda.contracts.testing
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.contracts.DealState
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.schemas.DummyDealStateSchemaV1
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -20,7 +22,8 @@ class DummyDealContract : Contract {
|
||||
override val contract: Contract = DummyDealContract(),
|
||||
override val participants: List<AbstractParty> = listOf(),
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(),
|
||||
override val ref: String) : DealState {
|
||||
override val ref: String) : DealState, QueryableState
|
||||
{
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return participants.any { it.owningKey.containsAny(ourKeys) }
|
||||
}
|
||||
@ -28,5 +31,17 @@ class DummyDealContract : Contract {
|
||||
override fun generateAgreement(notary: Party): TransactionBuilder {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(DummyDealStateSchemaV1)
|
||||
|
||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||
return when (schema) {
|
||||
is DummyDealStateSchemaV1 -> DummyDealStateSchemaV1.PersistentDummyDealState(
|
||||
uid = linearId,
|
||||
dealReference = ref
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -410,7 +410,7 @@ interface DealState : LinearState {
|
||||
}
|
||||
|
||||
// TODO: Remove this from the interface
|
||||
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(dealPartyName = listOf(<String>)))"))
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(dealPartyName = listOf(<String>)))"))
|
||||
inline fun <reified T : DealState> VaultService.dealsWith(party: AbstractParty) = linearHeadsOfType<T>().values.filter {
|
||||
it.state.data.participants.any { it == party }
|
||||
}
|
||||
|
@ -115,12 +115,14 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(),
|
||||
issuerRef = this.amount.token.issuer.reference.bytes
|
||||
)
|
||||
/** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
|
||||
/** Object Relational Mapping support. */
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(CashSchemaV1)
|
||||
/** Additional used schemas would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
|
@ -1,30 +0,0 @@
|
||||
package net.corda.contracts.testing
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.contracts.clauses.FilterOn
|
||||
import net.corda.core.contracts.clauses.verifyClause
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import java.security.PublicKey
|
||||
|
||||
class DummyLinearContract : Contract {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("Test")
|
||||
|
||||
val clause: Clause<State, CommandData, Unit> = LinearState.ClauseVerifier()
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx,
|
||||
FilterOn(clause, { states -> states.filterIsInstance<State>() }),
|
||||
emptyList())
|
||||
|
||||
data class State(
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(),
|
||||
override val contract: Contract = DummyLinearContract(),
|
||||
override val participants: List<AbstractParty> = listOf(),
|
||||
val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState {
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return participants.any { it.owningKey.containsAny(ourKeys) }
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,10 @@
|
||||
|
||||
package net.corda.contracts.testing
|
||||
|
||||
import net.corda.contracts.Commodity
|
||||
import net.corda.contracts.DealState
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
|
||||
import net.corda.contracts.DummyDealContract
|
||||
import net.corda.contracts.asset.*
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
@ -14,22 +14,25 @@ import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.CHARLIE
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.time.Instant.now
|
||||
import java.util.*
|
||||
|
||||
@JvmOverloads
|
||||
fun ServiceHub.fillWithSomeTestDeals(dealIds: List<String>,
|
||||
participants: List<AbstractParty> = emptyList()) : Vault<DealState> {
|
||||
val freshKey = keyManagementService.freshKey()
|
||||
val recipient = AnonymousParty(freshKey)
|
||||
val myKey: PublicKey = myInfo.legalIdentity.owningKey
|
||||
val me = AnonymousParty(myKey)
|
||||
|
||||
val transactions: List<SignedTransaction> = dealIds.map {
|
||||
// Issue a deal state
|
||||
val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(recipient)))
|
||||
addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(me)))
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
return@map signInitialTransaction(dummyIssue)
|
||||
@ -47,15 +50,24 @@ fun ServiceHub.fillWithSomeTestDeals(dealIds: List<String>,
|
||||
|
||||
@JvmOverloads
|
||||
fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int,
|
||||
uid: UniqueIdentifier = UniqueIdentifier(),
|
||||
participants: List<AbstractParty> = emptyList()) : Vault<LinearState> {
|
||||
val freshKey = keyManagementService.freshKey()
|
||||
val recipient = AnonymousParty(freshKey)
|
||||
externalId: String? = null,
|
||||
participants: List<AbstractParty> = emptyList(),
|
||||
linearString: String = "",
|
||||
linearNumber: Long = 0L,
|
||||
linearBoolean: Boolean = false,
|
||||
linearTimestamp: Instant = now()) : Vault<LinearState> {
|
||||
val myKey: PublicKey = myInfo.legalIdentity.owningKey
|
||||
val me = AnonymousParty(myKey)
|
||||
|
||||
val transactions: List<SignedTransaction> = (1..numberToCreate).map {
|
||||
// Issue a Linear state
|
||||
val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyLinearContract.State(linearId = uid, participants = participants.plus(recipient)))
|
||||
addOutputState(DummyLinearContract.State(linearId = UniqueIdentifier(externalId),
|
||||
participants = participants.plus(me),
|
||||
linearString = linearString,
|
||||
linearNumber = linearNumber,
|
||||
linearBoolean = linearBoolean,
|
||||
linearTimestamp = linearTimestamp))
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
|
||||
@ -116,6 +128,27 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
|
||||
return Vault(states)
|
||||
}
|
||||
|
||||
// TODO: need to make all FungibleAsset commands (issue, move, exit) generic
|
||||
fun ServiceHub.fillWithSomeTestCommodity(amount: Amount<Commodity>,
|
||||
outputNotary: Party = DUMMY_NOTARY,
|
||||
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
ownedBy: AbstractParty? = null,
|
||||
issuedBy: PartyAndReference = DUMMY_OBLIGATION_ISSUER.ref(1),
|
||||
issuerKey: KeyPair = DUMMY_OBLIGATION_ISSUER_KEY): Vault<CommodityContract.State> {
|
||||
val myKey: PublicKey = ownedBy?.owningKey ?: myInfo.legalIdentity.owningKey
|
||||
val me = AnonymousParty(myKey)
|
||||
|
||||
val commodity = CommodityContract()
|
||||
val issuance = TransactionType.General.Builder(null as Party?)
|
||||
commodity.generateIssue(issuance, Amount(amount.quantity, Issued(issuedBy.copy(reference = ref), amount.token)), me, outputNotary)
|
||||
issuance.signWith(issuerKey)
|
||||
val transaction = issuance.toSignedTransaction(true)
|
||||
|
||||
recordTransactions(transaction)
|
||||
|
||||
return Vault(setOf(transaction.tx.outRef<CommodityContract.State>(0)))
|
||||
}
|
||||
|
||||
fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray {
|
||||
val numSlots = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
|
||||
val baseSize = howMuch.quantity / numSlots
|
||||
@ -143,3 +176,58 @@ fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int,
|
||||
|
||||
return amounts
|
||||
}
|
||||
|
||||
fun <T : LinearState> ServiceHub.consume(states: List<StateAndRef<T>>) {
|
||||
// Create a txn consuming different contract types
|
||||
states.forEach {
|
||||
val consumedTx = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addInputState(it)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
|
||||
recordTransactions(consumedTx)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : LinearState> ServiceHub.consumeAndProduce(stateAndRef: StateAndRef<T>): StateAndRef<T> {
|
||||
// Create a txn consuming different contract types
|
||||
val consumedTx = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addInputState(stateAndRef)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
|
||||
recordTransactions(consumedTx)
|
||||
|
||||
// Create a txn consuming different contract types
|
||||
val producedTx = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyLinearContract.State(linearId = stateAndRef.state.data.linearId,
|
||||
participants = stateAndRef.state.data.participants))
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
|
||||
recordTransactions(producedTx)
|
||||
|
||||
return producedTx.tx.outRef<T>(0)
|
||||
}
|
||||
|
||||
fun <T : LinearState> ServiceHub.consumeAndProduce(states: List<StateAndRef<T>>) {
|
||||
states.forEach {
|
||||
consumeAndProduce(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun ServiceHub.consumeDeals(dealStates: List<StateAndRef<DealState>>) = consume(dealStates)
|
||||
fun ServiceHub.consumeLinearStates(linearStates: List<StateAndRef<LinearState>>) = consume(linearStates)
|
||||
fun ServiceHub.evolveLinearStates(linearStates: List<StateAndRef<LinearState>>) = consumeAndProduce(linearStates)
|
||||
fun ServiceHub.evolveLinearState(linearState: StateAndRef<LinearState>) : StateAndRef<LinearState> = consumeAndProduce(linearState)
|
||||
|
||||
@JvmOverloads
|
||||
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE) {
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||
vaultService.generateSpend(this, amount, to)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction(checkSufficientSignatures = false)
|
||||
|
||||
recordTransactions(spendTX)
|
||||
}
|
||||
|
@ -2,10 +2,8 @@ package net.corda.schemas
|
||||
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Index
|
||||
import javax.persistence.Table
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [CashSchema] family name (i.e. independent of version).
|
||||
@ -18,7 +16,7 @@ object CashSchema
|
||||
*/
|
||||
object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "cash_states",
|
||||
@Table(name = "contract_cash_states",
|
||||
indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"),
|
||||
Index(name = "pennies_idx", columnList = "pennies")))
|
||||
class PersistentCashState(
|
||||
|
@ -20,9 +20,9 @@ object CommercialPaperSchema
|
||||
object CommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "cp_states",
|
||||
indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"),
|
||||
Index(name = "maturity_idx", columnList = "maturity_instant"),
|
||||
Index(name = "face_value_idx", columnList = "face_value")))
|
||||
indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"),
|
||||
Index(name = "maturity_index", columnList = "maturity_instant"),
|
||||
Index(name = "face_value_index", columnList = "face_value")))
|
||||
class PersistentCommercialPaperState(
|
||||
@Column(name = "issuance_key")
|
||||
var issuanceParty: String,
|
||||
|
@ -0,0 +1,132 @@
|
||||
package net.corda.contracts.asset
|
||||
|
||||
import net.corda.contracts.clause.AbstractConserveAmount
|
||||
import net.corda.contracts.clause.AbstractIssue
|
||||
import net.corda.contracts.clause.NoZeroSizedOutputs
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.AllOf
|
||||
import net.corda.core.contracts.clauses.FirstOf
|
||||
import net.corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import net.corda.core.contracts.clauses.verifyClause
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.schemas.SampleCashSchemaV1
|
||||
import net.corda.schemas.SampleCashSchemaV2
|
||||
import net.corda.schemas.SampleCashSchemaV3
|
||||
import java.util.*
|
||||
|
||||
class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Commands, DummyFungibleContract.State>() {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
|
||||
|
||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<DummyFungibleContract.Commands>>
|
||||
= commands.select<DummyFungibleContract.Commands>()
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllOf<State, Commands, Issued<Currency>>(
|
||||
NoZeroSizedOutputs<State, Commands, Currency>(),
|
||||
FirstOf<State, Commands, Issued<Currency>>(
|
||||
Issue(),
|
||||
ConserveAmount())
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
|
||||
= tx.groupStates<State, Issued<Currency>> { it.amount.token }
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Commands, Currency>(
|
||||
sum = { sumCash() },
|
||||
sumOrZero = { sumCashOrZero(it) }
|
||||
) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
|
||||
}
|
||||
|
||||
data class State(
|
||||
override val amount: Amount<Issued<Currency>>,
|
||||
|
||||
override val owner: AbstractParty
|
||||
) : FungibleAsset<Currency>, QueryableState {
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: AbstractParty)
|
||||
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
||||
|
||||
override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey)
|
||||
override val contract = CASH_PROGRAM_ID
|
||||
override val participants = listOf(owner)
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty): FungibleAsset<Currency>
|
||||
= copy(amount = amount.copy(newAmount.quantity), owner = newOwner)
|
||||
|
||||
override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)"
|
||||
|
||||
override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
|
||||
/** Object Relational Mapping support. */
|
||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||
return when (schema) {
|
||||
is SampleCashSchemaV1 -> SampleCashSchemaV1.PersistentCashState(
|
||||
owner = this.owner.owningKey.toBase58String(),
|
||||
pennies = this.amount.quantity,
|
||||
currency = this.amount.token.product.currencyCode,
|
||||
issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(),
|
||||
issuerRef = this.amount.token.issuer.reference.bytes
|
||||
)
|
||||
is SampleCashSchemaV2 -> SampleCashSchemaV2.PersistentCashState(
|
||||
_participants = this.participants.toSet(),
|
||||
_owner = this.owner,
|
||||
_quantity = this.amount.quantity,
|
||||
currency = this.amount.token.product.currencyCode,
|
||||
_issuerParty = this.amount.token.issuer.party,
|
||||
_issuerRef = this.amount.token.issuer.reference.bytes
|
||||
)
|
||||
is SampleCashSchemaV3 -> SampleCashSchemaV3.PersistentCashState(
|
||||
_participants = this.participants.toSet(),
|
||||
_owner = this.owner,
|
||||
_quantity = this.amount.quantity,
|
||||
_currency = this.amount.token.product.currencyCode,
|
||||
_issuerParty = this.amount.token.issuer.party,
|
||||
_issuerRef = this.amount.token.issuer.reference.bytes
|
||||
)
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
|
||||
/** Object Relational Mapping support. */
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(SampleCashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3)
|
||||
}
|
||||
|
||||
interface Commands : FungibleAsset.Commands {
|
||||
|
||||
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
||||
|
||||
data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands
|
||||
|
||||
data class Exit(override val amount: Amount<Issued<Currency>>) : Commands, FungibleAsset.Commands.Exit<Currency>
|
||||
}
|
||||
|
||||
fun generateIssue(tx: TransactionBuilder, tokenDef: Issued<Currency>, pennies: Long, owner: AbstractParty, notary: Party)
|
||||
= generateIssue(tx, Amount(pennies, tokenDef), owner, notary)
|
||||
|
||||
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Currency>>, owner: AbstractParty, notary: Party)
|
||||
= generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand())
|
||||
|
||||
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
|
||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
||||
|
||||
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
|
||||
override fun generateIssueCommand() = Commands.Issue()
|
||||
override fun generateMoveCommand() = Commands.Move()
|
||||
|
||||
override fun verify(tx: TransactionForContract)
|
||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
package net.corda.schemas
|
||||
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [CashSchema] family name (i.e. independent of version).
|
||||
*/
|
||||
object CashSchema
|
||||
|
||||
/**
|
||||
* First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood
|
||||
* at the time of writing.
|
||||
*/
|
||||
object SampleCashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "contract_cash_states",
|
||||
indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"),
|
||||
Index(name = "pennies_idx", columnList = "pennies")))
|
||||
class PersistentCashState(
|
||||
@Column(name = "owner_key")
|
||||
var owner: String,
|
||||
|
||||
@Column(name = "pennies")
|
||||
var pennies: Long,
|
||||
|
||||
@Column(name = "ccy_code", length = 3)
|
||||
var currency: String,
|
||||
|
||||
@Column(name = "issuer_key")
|
||||
var issuerParty: String,
|
||||
|
||||
@Column(name = "issuer_ref")
|
||||
var issuerRef: ByteArray
|
||||
) : PersistentState()
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package net.corda.schemas
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Index
|
||||
import javax.persistence.Table
|
||||
|
||||
/**
|
||||
* Second version of a cash contract ORM schema that extends the common
|
||||
* [VaultFungibleState] abstract schema
|
||||
*/
|
||||
object SampleCashSchemaV2 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 2,
|
||||
mappedTypes = listOf(PersistentCashState::class.java, CommonSchemaV1.Party::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "cash_states_v2",
|
||||
indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code")))
|
||||
class PersistentCashState (
|
||||
/** product type */
|
||||
@Column(name = "ccy_code", length = 3)
|
||||
var currency: String,
|
||||
|
||||
/** parent attributes */
|
||||
@Transient
|
||||
val _participants: Set<AbstractParty>,
|
||||
@Transient
|
||||
val _owner: AbstractParty,
|
||||
@Transient
|
||||
val _quantity: Long,
|
||||
@Transient
|
||||
val _issuerParty: AbstractParty,
|
||||
@Transient
|
||||
val _issuerRef: ByteArray
|
||||
) : CommonSchemaV1.FungibleState(_participants, _owner, _quantity, _issuerParty, _issuerRef)
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package net.corda.schemas
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood
|
||||
* at the time of writing.
|
||||
*/
|
||||
object SampleCashSchemaV3 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 3,
|
||||
mappedTypes = listOf(PersistentCashState::class.java, CommonSchemaV1.Party::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "cash_states_v3")
|
||||
class PersistentCashState(
|
||||
/** [ContractState] attributes */
|
||||
@OneToMany(cascade = arrayOf(CascadeType.ALL))
|
||||
var participants: Set<CommonSchemaV1.Party>,
|
||||
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var owner: CommonSchemaV1.Party,
|
||||
|
||||
@Column(name = "pennies")
|
||||
var pennies: Long,
|
||||
|
||||
@Column(name = "ccy_code", length = 3)
|
||||
var currency: String,
|
||||
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var issuerParty: CommonSchemaV1.Party,
|
||||
|
||||
@Column(name = "issuer_ref")
|
||||
var issuerRef: ByteArray
|
||||
) : PersistentState() {
|
||||
constructor(_participants: Set<AbstractParty>, _owner: AbstractParty, _quantity: Long, _currency: String, _issuerParty: AbstractParty, _issuerRef: ByteArray)
|
||||
: this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(),
|
||||
owner = CommonSchemaV1.Party(_owner),
|
||||
pennies = _quantity,
|
||||
currency = _currency,
|
||||
issuerParty = CommonSchemaV1.Party(_issuerParty),
|
||||
issuerRef = _issuerRef)
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package net.corda.schemas
|
||||
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import java.time.Instant
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Index
|
||||
import javax.persistence.Table
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [CommercialPaperSchema] family name (i.e. independent of version).
|
||||
*/
|
||||
object CommercialPaperSchema
|
||||
|
||||
/**
|
||||
* First version of a commercial paper contract ORM schema that maps all fields of the [CommercialPaper] contract state
|
||||
* as it stood at the time of writing.
|
||||
*/
|
||||
object SampleCommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "cp_states",
|
||||
indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"),
|
||||
Index(name = "maturity_index", columnList = "maturity_instant"),
|
||||
Index(name = "face_value_index", columnList = "face_value")))
|
||||
class PersistentCommercialPaperState(
|
||||
@Column(name = "issuance_key")
|
||||
var issuanceParty: String,
|
||||
|
||||
@Column(name = "issuance_ref")
|
||||
var issuanceRef: ByteArray,
|
||||
|
||||
@Column(name = "owner_key")
|
||||
var owner: String,
|
||||
|
||||
@Column(name = "maturity_instant")
|
||||
var maturity: Instant,
|
||||
|
||||
@Column(name = "face_value")
|
||||
var faceValue: Long,
|
||||
|
||||
@Column(name = "ccy_code", length = 3)
|
||||
var currency: String,
|
||||
|
||||
@Column(name = "face_value_issuer_key")
|
||||
var faceValueIssuerParty: String,
|
||||
|
||||
@Column(name = "face_value_issuer_ref")
|
||||
var faceValueIssuerRef: ByteArray
|
||||
) : PersistentState()
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package net.corda.schemas
|
||||
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Index
|
||||
import javax.persistence.Table
|
||||
|
||||
/**
|
||||
* Second version of a cash contract ORM schema that extends the common
|
||||
* [VaultFungibleState] abstract schema
|
||||
*/
|
||||
object SampleCommercialPaperSchemaV2 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1,
|
||||
mappedTypes = listOf(PersistentCommercialPaperState::class.java, CommonSchemaV1.Party::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "cp_states_v2",
|
||||
indexes = arrayOf(Index(name = "ccy_code_index2", columnList = "ccy_code"),
|
||||
Index(name = "maturity_index2", columnList = "maturity_instant")))
|
||||
class PersistentCommercialPaperState(
|
||||
@Column(name = "maturity_instant")
|
||||
var maturity: Instant,
|
||||
|
||||
@Column(name = "ccy_code", length = 3)
|
||||
var currency: String,
|
||||
|
||||
@Column(name = "face_value_issuer_key")
|
||||
var faceValueIssuerParty: String,
|
||||
|
||||
@Column(name = "face_value_issuer_ref")
|
||||
var faceValueIssuerRef: ByteArray,
|
||||
|
||||
/** parent attributes */
|
||||
@Transient
|
||||
val _participants: Set<AbstractParty>,
|
||||
@Transient
|
||||
val _owner: AbstractParty,
|
||||
@Transient
|
||||
// face value
|
||||
val _quantity: Long,
|
||||
@Transient
|
||||
val _issuerParty: AbstractParty,
|
||||
@Transient
|
||||
val _issuerRef: ByteArray
|
||||
) : CommonSchemaV1.FungibleState(_participants, _owner, _quantity, _issuerParty, _issuerRef)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.corda.node.services.persistence.schemas
|
||||
package net.corda.node.services.persistence.schemas.requery
|
||||
|
||||
import io.requery.*
|
||||
import net.corda.core.crypto.SecureHash
|
@ -1,4 +1,4 @@
|
||||
package net.corda.node.services.vault.schemas
|
||||
package net.corda.node.services.vault.schemas.requery
|
||||
|
||||
import io.requery.*
|
||||
import net.corda.core.node.services.Vault
|
||||
@ -74,20 +74,4 @@ object VaultSchema {
|
||||
@get:Column(name = "lock_timestamp", nullable = true)
|
||||
var lockUpdateTime: Instant?
|
||||
}
|
||||
|
||||
/**
|
||||
* The following entity is for illustration purposes only as used by VaultQueryTests
|
||||
*/
|
||||
@Table(name = "vault_linear_states")
|
||||
@Entity(model = "vault")
|
||||
interface VaultLinearState : Persistable {
|
||||
|
||||
@get:Index("external_id_index")
|
||||
@get:Column(name = "external_id")
|
||||
var externalId: String
|
||||
|
||||
@get:Index("uuid_index")
|
||||
@get:Column(name = "uuid", unique = true, nullable = false)
|
||||
var uuid: UUID
|
||||
}
|
||||
}
|
@ -2,15 +2,13 @@ package net.corda.node.services.vault.schemas
|
||||
|
||||
import io.requery.Persistable
|
||||
import io.requery.TransactionIsolation
|
||||
import io.requery.kotlin.`in`
|
||||
import io.requery.kotlin.eq
|
||||
import io.requery.kotlin.invoke
|
||||
import io.requery.kotlin.isNull
|
||||
import io.requery.kotlin.*
|
||||
import io.requery.query.RowExpression
|
||||
import io.requery.rx.KotlinRxEntityStore
|
||||
import io.requery.sql.*
|
||||
import io.requery.sql.platform.Generic
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
@ -23,10 +21,8 @@ import net.corda.core.schemas.requery.converters.VaultStateStatusConverter
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.ALICE
|
||||
import net.corda.core.utilities.BOB
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.node.services.vault.schemas.requery.*
|
||||
import org.h2.jdbcx.JdbcDataSource
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
@ -34,7 +30,6 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
@ -56,7 +51,7 @@ class VaultSchemaTest {
|
||||
fun setup() {
|
||||
val dataSource = JdbcDataSource()
|
||||
dataSource.setURL("jdbc:h2:mem:vault_persistence;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1")
|
||||
val configuration = KotlinConfiguration(dataSource = dataSource, model = Models.VAULT, mapping = setupCustomMapping())
|
||||
val configuration = KotlinConfiguration(dataSource = dataSource, model = Models.VAULT, mapping = setupCustomMapping(), useDefaultLogging = true)
|
||||
instance = KotlinEntityDataStore<Persistable>(configuration)
|
||||
oinstance = KotlinRxEntityStore(KotlinEntityDataStore<Persistable>(configuration))
|
||||
val tables = SchemaModifier(configuration)
|
||||
@ -102,7 +97,10 @@ class VaultSchemaTest {
|
||||
}
|
||||
|
||||
private fun setupDummyData() {
|
||||
// dummy Transaction
|
||||
// dummy Transaction comprised of 3 different Contract State types
|
||||
// 1. SingleOwnerState
|
||||
// 2. MultiOwnerState
|
||||
// 3. VaultNoopState
|
||||
val notary: Party = DUMMY_NOTARY
|
||||
val inState1 = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary)
|
||||
val inState2 = TransactionState(DummyContract.MultiOwnerState(0,
|
||||
@ -287,6 +285,24 @@ class VaultSchemaTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDistinctContractStateTypes() {
|
||||
val txn = createTxnWithTwoStateTypes()
|
||||
dummyStatesInsert(txn)
|
||||
|
||||
data.invoke {
|
||||
transaction!!.inputs.forEach {
|
||||
val stateEntity = createStateEntity(it)
|
||||
insert(stateEntity)
|
||||
}
|
||||
|
||||
val query = select(VaultSchema.VaultStates::contractStateClassName).distinct()
|
||||
val results = query.get()
|
||||
|
||||
Assert.assertSame(3, results.count())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createStateEntity(stateAndRef: StateAndRef<*>, idx: Int? = null, txHash: String? = null): VaultStatesEntity {
|
||||
val stateRef = stateAndRef.ref
|
||||
val state = stateAndRef.state
|
||||
|
@ -1,4 +1,7 @@
|
||||
apply plugin: 'kotlin'
|
||||
// Java Persistence API support: create no-arg constructor
|
||||
// see: http://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell
|
||||
apply plugin: 'kotlin-jpa'
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
@ -137,6 +140,9 @@ dependencies {
|
||||
testCompile project(':test-utils')
|
||||
testCompile project(':client:jfx')
|
||||
|
||||
// sample test schemas
|
||||
testCompile project(path: ':finance', configuration: 'testArtifacts')
|
||||
|
||||
// For H2 database support in persistence
|
||||
compile "com.h2database:h2:$h2_version"
|
||||
|
||||
|
@ -22,6 +22,7 @@ import net.corda.node.utilities.ServiceIdentityGenerator
|
||||
import net.corda.node.utilities.transaction
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.*
|
||||
@ -52,11 +53,13 @@ class BFTNotaryServiceTests : NodeBasedTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Under investigation due to failure on TC build server")
|
||||
fun `detect double spend 1 faulty`() {
|
||||
detectDoubleSpend(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Under investigation due to failure on TC build server")
|
||||
fun `detect double spend 2 faulty`() {
|
||||
detectDoubleSpend(2)
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import net.corda.node.services.api.*
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.events.NodeSchedulerService
|
||||
import net.corda.node.services.events.ScheduledActivityObserver
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
@ -51,6 +52,7 @@ import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.services.statemachine.flowVersionAndInitiatingClass
|
||||
import net.corda.node.services.transactions.*
|
||||
import net.corda.node.services.vault.CashBalanceAsMetricsObserver
|
||||
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.services.vault.VaultSoftLockManager
|
||||
import net.corda.node.utilities.AddOrRemove.ADD
|
||||
@ -121,6 +123,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
override val networkMapCache: NetworkMapCacheInternal get() = netMapCache
|
||||
override val storageService: TxWritableStorageService get() = storage
|
||||
override val vaultService: VaultService get() = vault
|
||||
override val vaultQueryService: VaultQueryService get() = vaultQuery
|
||||
override val keyManagementService: KeyManagementService get() = keyManagement
|
||||
override val identityService: IdentityService get() = identity
|
||||
override val schedulerService: SchedulerService get() = scheduler
|
||||
@ -164,6 +167,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
lateinit var checkpointStorage: CheckpointStorage
|
||||
lateinit var smm: StateMachineManager
|
||||
lateinit var vault: VaultService
|
||||
lateinit var vaultQuery: VaultQueryService
|
||||
lateinit var keyManagement: KeyManagementService
|
||||
var inNodeNetworkMapService: NetworkMapService? = null
|
||||
lateinit var txVerifierService: TransactionVerifierService
|
||||
@ -447,6 +451,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
network = makeMessagingService()
|
||||
schemas = makeSchemaService()
|
||||
vault = makeVaultService(configuration.dataSourceProperties)
|
||||
vaultQuery = makeVaultQueryService(schemas)
|
||||
txVerifierService = makeTransactionVerifierService()
|
||||
auditService = DummyAuditService()
|
||||
|
||||
@ -525,7 +530,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
VaultSoftLockManager(vault, smm)
|
||||
CashBalanceAsMetricsObserver(services, database)
|
||||
ScheduledActivityObserver(services)
|
||||
HibernateObserver(vault.rawUpdates, schemas)
|
||||
HibernateObserver(vault.rawUpdates, HibernateConfiguration(schemas))
|
||||
}
|
||||
|
||||
private fun makeInfo(): NodeInfo {
|
||||
@ -704,7 +709,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
// TODO: sort out ordering of open & protected modifiers of functions in this class.
|
||||
protected open fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties)
|
||||
|
||||
protected open fun makeSchemaService(): SchemaService = NodeSchemaService()
|
||||
protected open fun makeVaultQueryService(schemas: SchemaService): VaultQueryService = HibernateVaultQueryImpl(HibernateConfiguration(schemas), vault.updatesPublisher)
|
||||
|
||||
protected open fun makeSchemaService(): SchemaService = NodeSchemaService(pluginRegistries.flatMap { it.requiredSchemas }.toSet())
|
||||
|
||||
protected abstract fun makeTransactionVerifierService(): TransactionVerifierService
|
||||
|
||||
|
@ -14,6 +14,7 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
@ -59,7 +60,7 @@ class CordaRPCOpsImpl(
|
||||
paging: PageSpecification,
|
||||
sorting: Sort): Vault.Page<T> {
|
||||
return database.transaction {
|
||||
services.vaultService.queryBy<T>(criteria, paging, sorting)
|
||||
services.vaultQueryService._queryBy(criteria, paging, sorting, ContractState::class.java as Class<T>)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +69,7 @@ class CordaRPCOpsImpl(
|
||||
paging: PageSpecification,
|
||||
sorting: Sort): Vault.PageAndUpdates<T> {
|
||||
return database.transaction {
|
||||
services.vaultService.trackBy<T>(criteria, paging, sorting)
|
||||
services.vaultQueryService._trackBy<T>(criteria, paging, sorting, ContractState::class.java as Class<T>)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.corda.node.services.api
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
|
||||
//DOCSTART SchemaService
|
||||
/**
|
||||
@ -23,12 +23,12 @@ interface SchemaService {
|
||||
* Given a state, select schemas to map it to that are supported by [generateMappedObject] and that are configured
|
||||
* for this node.
|
||||
*/
|
||||
fun selectSchemas(state: QueryableState): Iterable<MappedSchema>
|
||||
fun selectSchemas(state: ContractState): Iterable<MappedSchema>
|
||||
|
||||
/**
|
||||
* Map a state to a [PersistentState] for the given schema, either via direct support from the state
|
||||
* or via custom logic in this service.
|
||||
*/
|
||||
fun generateMappedObject(state: QueryableState, schema: MappedSchema): PersistentState
|
||||
fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState
|
||||
}
|
||||
//DOCEND SchemaService
|
||||
|
@ -0,0 +1,119 @@
|
||||
package net.corda.node.services.database
|
||||
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import org.hibernate.SessionFactory
|
||||
import org.hibernate.boot.MetadataSources
|
||||
import org.hibernate.boot.model.naming.Identifier
|
||||
import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
||||
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
|
||||
import org.hibernate.cfg.Configuration
|
||||
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider
|
||||
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment
|
||||
import org.hibernate.service.UnknownUnwrapTypeException
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import java.sql.Connection
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class HibernateConfiguration(val schemaService: SchemaService, val useDefaultLogging: Boolean = false) {
|
||||
constructor(schemaService: SchemaService) : this(schemaService, false)
|
||||
|
||||
companion object {
|
||||
val logger = loggerFor<HibernateConfiguration>()
|
||||
}
|
||||
|
||||
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
|
||||
val sessionFactories = ConcurrentHashMap<MappedSchema, SessionFactory>()
|
||||
|
||||
init {
|
||||
schemaService.schemaOptions.map { it.key }.forEach { mappedSchema ->
|
||||
sessionFactories.computeIfAbsent(mappedSchema, { makeSessionFactoryForSchema(mappedSchema) })
|
||||
}
|
||||
}
|
||||
|
||||
fun sessionFactoryForRegisteredSchemas(): SessionFactory {
|
||||
return sessionFactoryForSchemas(*schemaService.schemaOptions.map { it.key }.toTypedArray())
|
||||
}
|
||||
|
||||
fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory {
|
||||
return sessionFactories.computeIfAbsent(schema, { sessionFactoryForSchemas(schema) })
|
||||
}
|
||||
|
||||
fun sessionFactoryForSchemas(vararg schemas: MappedSchema): SessionFactory {
|
||||
return makeSessionFactoryForSchemas(schemas.iterator())
|
||||
}
|
||||
|
||||
private fun makeSessionFactoryForSchema(schema: MappedSchema): SessionFactory {
|
||||
return makeSessionFactoryForSchemas(setOf(schema).iterator())
|
||||
}
|
||||
|
||||
private fun makeSessionFactoryForSchemas(schemas: Iterator<MappedSchema>): SessionFactory {
|
||||
logger.info("Creating session factory for schemas: $schemas")
|
||||
val serviceRegistry = BootstrapServiceRegistryBuilder().build()
|
||||
val metadataSources = MetadataSources(serviceRegistry)
|
||||
// We set a connection provider as the auto schema generation requires it. The auto schema generation will not
|
||||
// necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though.
|
||||
// TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs.
|
||||
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name)
|
||||
.setProperty("hibernate.hbm2ddl.auto", "update")
|
||||
.setProperty("hibernate.show_sql", "$useDefaultLogging")
|
||||
.setProperty("hibernate.format_sql", "$useDefaultLogging")
|
||||
schemas.forEach { schema ->
|
||||
// TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session
|
||||
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
|
||||
}
|
||||
val sessionFactory = buildSessionFactory(config, metadataSources, "")
|
||||
logger.info("Created session factory for schemas: $schemas")
|
||||
return sessionFactory
|
||||
}
|
||||
|
||||
private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, tablePrefix: String): SessionFactory {
|
||||
config.standardServiceRegistryBuilder.applySettings(config.properties)
|
||||
val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run {
|
||||
applyPhysicalNamingStrategy(object : PhysicalNamingStrategyStandardImpl() {
|
||||
override fun toPhysicalTableName(name: Identifier?, context: JdbcEnvironment?): Identifier {
|
||||
val default = super.toPhysicalTableName(name, context)
|
||||
return Identifier.toIdentifier(tablePrefix + default.text, default.isQuoted)
|
||||
}
|
||||
})
|
||||
build()
|
||||
}
|
||||
|
||||
return metadata.sessionFactoryBuilder.run {
|
||||
allowOutOfTransactionUpdateOperations(true)
|
||||
applySecondLevelCacheSupport(false)
|
||||
applyQueryCacheSupport(false)
|
||||
enableReleaseResourcesOnCloseEnabled(true)
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
// Supply Hibernate with connections from our underlying Exposed database integration. Only used
|
||||
// during schema creation / update.
|
||||
class NodeDatabaseConnectionProvider : ConnectionProvider {
|
||||
override fun closeConnection(conn: Connection) {
|
||||
val tx = TransactionManager.current()
|
||||
tx.commit()
|
||||
tx.close()
|
||||
}
|
||||
|
||||
override fun supportsAggressiveRelease(): Boolean = true
|
||||
|
||||
override fun getConnection(): Connection {
|
||||
val tx = TransactionManager.manager.newTransaction(Connection.TRANSACTION_REPEATABLE_READ)
|
||||
return tx.connection
|
||||
}
|
||||
|
||||
override fun <T : Any?> unwrap(unwrapType: Class<T>): T {
|
||||
try {
|
||||
return unwrapType.cast(this)
|
||||
} catch(e: ClassCastException) {
|
||||
throw UnknownUnwrapTypeException(unwrapType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isUnwrappableAs(unwrapType: Class<*>?): Boolean = (unwrapType == NodeDatabaseConnectionProvider::class.java)
|
||||
}
|
||||
}
|
@ -21,8 +21,8 @@ import net.corda.core.serialization.SerializeAsTokenContext
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.api.AcceptsFileUpload
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.persistence.schemas.AttachmentEntity
|
||||
import net.corda.node.services.persistence.schemas.Models
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.services.persistence.schemas.requery.Models
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.FilterInputStream
|
||||
import java.io.IOException
|
||||
|
@ -9,70 +9,25 @@ import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import org.hibernate.FlushMode
|
||||
import org.hibernate.SessionFactory
|
||||
import org.hibernate.boot.MetadataSources
|
||||
import org.hibernate.boot.model.naming.Identifier
|
||||
import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
||||
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
|
||||
import org.hibernate.cfg.Configuration
|
||||
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider
|
||||
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment
|
||||
import org.hibernate.service.UnknownUnwrapTypeException
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import rx.Observable
|
||||
import java.sql.Connection
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* A vault observer that extracts Object Relational Mappings for contract states that support it, and persists them with Hibernate.
|
||||
*/
|
||||
// TODO: Manage version evolution of the schemas via additional tooling.
|
||||
class HibernateObserver(vaultUpdates: Observable<Vault.Update>, val schemaService: SchemaService) {
|
||||
class HibernateObserver(vaultUpdates: Observable<Vault.Update>, val config: HibernateConfiguration) {
|
||||
|
||||
companion object {
|
||||
val logger = loggerFor<HibernateObserver>()
|
||||
}
|
||||
|
||||
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
|
||||
val sessionFactories = ConcurrentHashMap<MappedSchema, SessionFactory>()
|
||||
|
||||
init {
|
||||
schemaService.schemaOptions.map { it.key }.forEach {
|
||||
makeSessionFactoryForSchema(it)
|
||||
}
|
||||
vaultUpdates.subscribe { persist(it.produced) }
|
||||
}
|
||||
|
||||
private fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory {
|
||||
return sessionFactories.computeIfAbsent(schema, { makeSessionFactoryForSchema(it) })
|
||||
}
|
||||
|
||||
private fun makeSessionFactoryForSchema(schema: MappedSchema): SessionFactory {
|
||||
logger.info("Creating session factory for schema $schema")
|
||||
val serviceRegistry = BootstrapServiceRegistryBuilder().build()
|
||||
val metadataSources = MetadataSources(serviceRegistry)
|
||||
// We set a connection provider as the auto schema generation requires it. The auto schema generation will not
|
||||
// necessarily remain and would likely be replaced by something like Liquibase. For now it is very convenient though.
|
||||
// TODO: replace auto schema generation as it isn't intended for production use, according to Hibernate docs.
|
||||
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", NodeDatabaseConnectionProvider::class.java.name)
|
||||
.setProperty("hibernate.hbm2ddl.auto", "update")
|
||||
.setProperty("hibernate.show_sql", "false")
|
||||
.setProperty("hibernate.format_sql", "true")
|
||||
val options = schemaService.schemaOptions[schema]
|
||||
val databaseSchema = options?.databaseSchema
|
||||
if (databaseSchema != null) {
|
||||
logger.debug { "Database schema = $databaseSchema" }
|
||||
config.setProperty("hibernate.default_schema", databaseSchema)
|
||||
}
|
||||
val tablePrefix = options?.tablePrefix ?: "contract_" // We always have this as the default for aesthetic reasons.
|
||||
logger.debug { "Table prefix = $tablePrefix" }
|
||||
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
|
||||
val sessionFactory = buildSessionFactory(config, metadataSources, tablePrefix)
|
||||
logger.info("Created session factory for schema $schema")
|
||||
return sessionFactory
|
||||
}
|
||||
|
||||
private fun persist(produced: Set<StateAndRef<ContractState>>) {
|
||||
produced.forEach { persistState(it) }
|
||||
}
|
||||
@ -81,69 +36,21 @@ class HibernateObserver(vaultUpdates: Observable<Vault.Update>, val schemaServic
|
||||
val state = stateAndRef.state.data
|
||||
if (state is QueryableState) {
|
||||
logger.debug { "Asked to persist state ${stateAndRef.ref}" }
|
||||
schemaService.selectSchemas(state).forEach { persistStateWithSchema(state, stateAndRef.ref, it) }
|
||||
config.schemaService.selectSchemas(state).forEach { persistStateWithSchema(state, stateAndRef.ref, it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistStateWithSchema(state: QueryableState, stateRef: StateRef, schema: MappedSchema) {
|
||||
val sessionFactory = sessionFactoryForSchema(schema)
|
||||
fun persistStateWithSchema(state: QueryableState, stateRef: StateRef, schema: MappedSchema) {
|
||||
val sessionFactory = config.sessionFactoryForSchema(schema)
|
||||
val session = sessionFactory.withOptions().
|
||||
connection(TransactionManager.current().connection).
|
||||
flushMode(FlushMode.MANUAL).
|
||||
openSession()
|
||||
session.use {
|
||||
val mappedObject = schemaService.generateMappedObject(state, schema)
|
||||
val mappedObject = config.schemaService.generateMappedObject(state, schema)
|
||||
mappedObject.stateRef = PersistentStateRef(stateRef)
|
||||
it.persist(mappedObject)
|
||||
it.flush()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, tablePrefix: String): SessionFactory {
|
||||
config.standardServiceRegistryBuilder.applySettings(config.properties)
|
||||
val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run {
|
||||
applyPhysicalNamingStrategy(object : PhysicalNamingStrategyStandardImpl() {
|
||||
override fun toPhysicalTableName(name: Identifier?, context: JdbcEnvironment?): Identifier {
|
||||
val default = super.toPhysicalTableName(name, context)
|
||||
return Identifier.toIdentifier(tablePrefix + default.text, default.isQuoted)
|
||||
}
|
||||
})
|
||||
build()
|
||||
}
|
||||
|
||||
return metadata.sessionFactoryBuilder.run {
|
||||
allowOutOfTransactionUpdateOperations(true)
|
||||
applySecondLevelCacheSupport(false)
|
||||
applyQueryCacheSupport(false)
|
||||
enableReleaseResourcesOnCloseEnabled(true)
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
// Supply Hibernate with connections from our underlying Exposed database integration. Only used
|
||||
// during schema creation / update.
|
||||
class NodeDatabaseConnectionProvider : ConnectionProvider {
|
||||
override fun closeConnection(conn: Connection) {
|
||||
val tx = TransactionManager.current()
|
||||
tx.commit()
|
||||
tx.close()
|
||||
}
|
||||
|
||||
override fun supportsAggressiveRelease(): Boolean = true
|
||||
|
||||
override fun getConnection(): Connection {
|
||||
val tx = TransactionManager.manager.newTransaction(Connection.TRANSACTION_REPEATABLE_READ)
|
||||
return tx.connection
|
||||
}
|
||||
|
||||
override fun <T : Any?> unwrap(unwrapType: Class<T>): T {
|
||||
try {
|
||||
return unwrapType.cast(this)
|
||||
} catch(e: ClassCastException) {
|
||||
throw UnknownUnwrapTypeException(unwrapType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isUnwrappableAs(unwrapType: Class<*>?): Boolean = (unwrapType == NodeDatabaseConnectionProvider::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,16 @@
|
||||
package net.corda.node.services.schema
|
||||
|
||||
import net.corda.contracts.DealState
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.FungibleAsset
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1
|
||||
import net.corda.schemas.CashSchemaV1
|
||||
|
||||
/**
|
||||
@ -15,21 +21,46 @@ import net.corda.schemas.CashSchemaV1
|
||||
* TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState].
|
||||
* TODO: create whitelisted tables when a CorDapp is first installed
|
||||
*/
|
||||
class NodeSchemaService : SchemaService, SingletonSerializeAsToken() {
|
||||
class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaService, SingletonSerializeAsToken() {
|
||||
|
||||
// Currently does not support configuring schema options.
|
||||
|
||||
// Whitelisted tables are those required by internal Corda services
|
||||
// For example, cash is used by the vault for coin selection
|
||||
// This whitelist will grow as we add further functionality (eg. other fungible assets)
|
||||
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = mapOf(Pair(CashSchemaV1, SchemaService.SchemaOptions()))
|
||||
// Required schemas are those used by internal Corda services
|
||||
// For example, cash is used by the vault for coin selection (but will be extracted as a standalone CorDapp in future)
|
||||
val requiredSchemas: Map<MappedSchema, SchemaService.SchemaOptions> =
|
||||
mapOf(Pair(CashSchemaV1, SchemaService.SchemaOptions()),
|
||||
Pair(CommonSchemaV1, SchemaService.SchemaOptions()),
|
||||
Pair(VaultSchemaV1, SchemaService.SchemaOptions()))
|
||||
|
||||
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas.plus(customSchemas.map {
|
||||
mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions())
|
||||
})
|
||||
|
||||
// Currently returns all schemas supported by the state, with no filtering or enrichment.
|
||||
override fun selectSchemas(state: QueryableState): Iterable<MappedSchema> {
|
||||
return state.supportedSchemas()
|
||||
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> {
|
||||
val schemas = mutableSetOf<MappedSchema>()
|
||||
if (state is QueryableState)
|
||||
schemas += state.supportedSchemas()
|
||||
if (state is LinearState)
|
||||
schemas += VaultSchemaV1 // VaultLinearStates
|
||||
// TODO: DealState to be deprecated (collapsed into LinearState)
|
||||
if (state is DealState)
|
||||
schemas += VaultSchemaV1 // VaultLinearStates
|
||||
if (state is FungibleAsset<*>)
|
||||
schemas += VaultSchemaV1 // VaultFungibleStates
|
||||
|
||||
return schemas
|
||||
}
|
||||
|
||||
// Because schema is always one supported by the state, just delegate.
|
||||
override fun generateMappedObject(state: QueryableState, schema: MappedSchema): PersistentState {
|
||||
return state.generateMappedObject(schema)
|
||||
override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState {
|
||||
// TODO: DealState to be deprecated (collapsed into LinearState)
|
||||
if ((schema is VaultSchemaV1) && (state is DealState))
|
||||
return VaultSchemaV1.VaultLinearStates(state.linearId, state.ref, state.participants)
|
||||
if ((schema is VaultSchemaV1) && (state is LinearState))
|
||||
return VaultSchemaV1.VaultLinearStates(state.linearId, "", state.participants)
|
||||
if ((schema is VaultSchemaV1) && (state is FungibleAsset<*>))
|
||||
return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants)
|
||||
return (state as QueryableState).generateMappedObject(schema)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,368 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultQueryException
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.serialization.toHexString
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.util.*
|
||||
import javax.persistence.Tuple
|
||||
import javax.persistence.criteria.*
|
||||
|
||||
|
||||
class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
|
||||
val contractTypeMappings: Map<String, List<String>>,
|
||||
val criteriaBuilder: CriteriaBuilder,
|
||||
val criteriaQuery: CriteriaQuery<Tuple>,
|
||||
val vaultStates: Root<VaultSchemaV1.VaultStates>) : IQueryCriteriaParser {
|
||||
private companion object {
|
||||
val log = loggerFor<HibernateQueryCriteriaParser>()
|
||||
}
|
||||
|
||||
// incrementally build list of join predicates
|
||||
private val joinPredicates = mutableListOf<Predicate>()
|
||||
// incrementally build list of root entities (for later use in Sort parsing)
|
||||
private val rootEntities = mutableMapOf<Class<out PersistentState>, Root<*>>()
|
||||
|
||||
var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED
|
||||
|
||||
override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria) : Collection<Predicate> {
|
||||
log.trace { "Parsing VaultQueryCriteria: $criteria" }
|
||||
val predicateSet = mutableSetOf<Predicate>()
|
||||
|
||||
// state status
|
||||
stateTypes = criteria.status
|
||||
if (criteria.status == Vault.StateStatus.ALL)
|
||||
predicateSet.add(vaultStates.get<Vault.StateStatus>("stateStatus").`in`(setOf(Vault.StateStatus.UNCONSUMED, Vault.StateStatus.CONSUMED)))
|
||||
else
|
||||
predicateSet.add(criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>("stateStatus"), criteria.status))
|
||||
|
||||
// contract State Types
|
||||
val combinedContractTypeTypes = criteria.contractStateTypes?.plus(contractType) ?: setOf(contractType)
|
||||
combinedContractTypeTypes.filter { it.name != ContractState::class.java.name }.let {
|
||||
val interfaces = it.flatMap { contractTypeMappings[it.name] ?: emptyList() }
|
||||
val concrete = it.filter { !it.isInterface }.map { it.name }
|
||||
val all = interfaces.plus(concrete)
|
||||
if (all.isNotEmpty())
|
||||
predicateSet.add(criteriaBuilder.and(vaultStates.get<String>("contractStateClassName").`in`(all)))
|
||||
}
|
||||
|
||||
// soft locking
|
||||
if (!criteria.includeSoftlockedStates)
|
||||
predicateSet.add(criteriaBuilder.and(vaultStates.get<String>("lockId").isNull))
|
||||
|
||||
// notary names
|
||||
criteria.notaryName?.let {
|
||||
val notaryNames = (criteria.notaryName as List<X500Name>).map { it.toString() }
|
||||
predicateSet.add(criteriaBuilder.and(vaultStates.get<String>("notaryName").`in`(notaryNames)))
|
||||
}
|
||||
|
||||
// state references
|
||||
criteria.stateRefs?.let {
|
||||
val persistentStateRefs = (criteria.stateRefs as List<StateRef>).map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
|
||||
val compositeKey = vaultStates.get<PersistentStateRef>("stateRef")
|
||||
predicateSet.add(criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)))
|
||||
}
|
||||
|
||||
// time constraints (recorded, consumed)
|
||||
criteria.timeCondition?.let {
|
||||
val timeCondition = criteria.timeCondition
|
||||
val timeInstantType = timeCondition!!.type
|
||||
val timeColumn = when (timeInstantType) {
|
||||
QueryCriteria.TimeInstantType.RECORDED -> Column.Kotlin(VaultSchemaV1.VaultStates::recordedTime)
|
||||
QueryCriteria.TimeInstantType.CONSUMED -> Column.Kotlin(VaultSchemaV1.VaultStates::consumedTime)
|
||||
}
|
||||
val expression = CriteriaExpression.ColumnPredicateExpression(timeColumn, timeCondition.predicate)
|
||||
predicateSet.add(expressionToPredicate(vaultStates, expression))
|
||||
}
|
||||
return predicateSet
|
||||
}
|
||||
|
||||
private fun columnPredicateToPredicate(column: Path<out Any?>, columnPredicate: ColumnPredicate<*>): Predicate {
|
||||
return when (columnPredicate) {
|
||||
is ColumnPredicate.EqualityComparison -> {
|
||||
val literal = columnPredicate.rightLiteral
|
||||
when (columnPredicate.operator) {
|
||||
EqualityComparisonOperator.EQUAL -> criteriaBuilder.equal(column, literal)
|
||||
EqualityComparisonOperator.NOT_EQUAL -> criteriaBuilder.notEqual(column, literal)
|
||||
}
|
||||
}
|
||||
is ColumnPredicate.BinaryComparison -> {
|
||||
column as Path<Comparable<Any?>?>
|
||||
val literal = columnPredicate.rightLiteral as Comparable<Any?>?
|
||||
when (columnPredicate.operator) {
|
||||
BinaryComparisonOperator.GREATER_THAN -> criteriaBuilder.greaterThan(column, literal)
|
||||
BinaryComparisonOperator.GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal)
|
||||
BinaryComparisonOperator.LESS_THAN -> criteriaBuilder.lessThan(column, literal)
|
||||
BinaryComparisonOperator.LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal)
|
||||
}
|
||||
}
|
||||
is ColumnPredicate.Likeness -> {
|
||||
column as Path<String?>
|
||||
when (columnPredicate.operator) {
|
||||
LikenessOperator.LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral)
|
||||
LikenessOperator.NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral)
|
||||
}
|
||||
}
|
||||
is ColumnPredicate.CollectionExpression -> {
|
||||
when (columnPredicate.operator) {
|
||||
CollectionOperator.IN -> column.`in`(columnPredicate.rightLiteral)
|
||||
CollectionOperator.NOT_IN -> criteriaBuilder.not(column.`in`(columnPredicate.rightLiteral))
|
||||
}
|
||||
}
|
||||
is ColumnPredicate.Between -> {
|
||||
column as Path<Comparable<Any?>?>
|
||||
val fromLiteral = columnPredicate.rightFromLiteral as Comparable<Any?>?
|
||||
val toLiteral = columnPredicate.rightToLiteral as Comparable<Any?>?
|
||||
criteriaBuilder.between(column, fromLiteral, toLiteral)
|
||||
}
|
||||
is ColumnPredicate.NullExpression -> {
|
||||
when (columnPredicate.operator) {
|
||||
NullOperator.IS_NULL -> criteriaBuilder.isNull(column)
|
||||
NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return : Expression<Boolean> -> : Predicate
|
||||
*/
|
||||
private fun <O, R> expressionToExpression(root: Root<O>, expression: CriteriaExpression<O, R>): Expression<R> {
|
||||
return when (expression) {
|
||||
is CriteriaExpression.BinaryLogical -> {
|
||||
val leftPredicate = expressionToExpression(root, expression.left)
|
||||
val rightPredicate = expressionToExpression(root, expression.right)
|
||||
when (expression.operator) {
|
||||
BinaryLogicalOperator.AND -> criteriaBuilder.and(leftPredicate, rightPredicate) as Expression<R>
|
||||
BinaryLogicalOperator.OR -> criteriaBuilder.or(leftPredicate, rightPredicate) as Expression<R>
|
||||
}
|
||||
}
|
||||
is CriteriaExpression.Not -> criteriaBuilder.not(expressionToExpression(root, expression.expression)) as Expression<R>
|
||||
is CriteriaExpression.ColumnPredicateExpression<O, *> -> {
|
||||
val column = root.get<Any?>(getColumnName(expression.column))
|
||||
columnPredicateToPredicate(column, expression.predicate) as Expression<R>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <O> expressionToPredicate(root: Root<O>, expression: CriteriaExpression<O, Boolean>): Predicate {
|
||||
return expressionToExpression(root, expression) as Predicate
|
||||
}
|
||||
|
||||
override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria) : Collection<Predicate> {
|
||||
log.trace { "Parsing FungibleAssetQueryCriteria: $criteria" }
|
||||
|
||||
var predicateSet = mutableSetOf<Predicate>()
|
||||
|
||||
val vaultFungibleStates = criteriaQuery.from(VaultSchemaV1.VaultFungibleStates::class.java)
|
||||
rootEntities.putIfAbsent(VaultSchemaV1.VaultFungibleStates::class.java, vaultFungibleStates)
|
||||
|
||||
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultFungibleStates.get<PersistentStateRef>("stateRef"))
|
||||
predicateSet.add(joinPredicate)
|
||||
|
||||
// owner
|
||||
criteria.owner?.let {
|
||||
val ownerKeys = criteria.owner as List<AbstractParty>
|
||||
val joinFungibleStateToParty = vaultFungibleStates.join<VaultSchemaV1.VaultFungibleStates, CommonSchemaV1.Party>("issuerParty")
|
||||
val owners = ownerKeys.map { it.nameOrNull()?.toString() ?: it.toString()}
|
||||
predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get<CommonSchemaV1.Party>("name").`in`(owners)))
|
||||
}
|
||||
|
||||
// quantity
|
||||
criteria.quantity?.let {
|
||||
predicateSet.add(columnPredicateToPredicate(vaultFungibleStates.get<Long>("quantity"), it))
|
||||
}
|
||||
|
||||
// issuer party
|
||||
criteria.issuerPartyName?.let {
|
||||
val issuerParties = criteria.issuerPartyName as List<AbstractParty>
|
||||
val joinFungibleStateToParty = vaultFungibleStates.join<VaultSchemaV1.VaultFungibleStates, CommonSchemaV1.Party>("issuerParty")
|
||||
val dealPartyKeys = issuerParties.map { it.nameOrNull().toString() }
|
||||
predicateSet.add(criteriaBuilder.equal(joinFungibleStateToParty.get<CommonSchemaV1.Party>("name"), dealPartyKeys))
|
||||
}
|
||||
|
||||
// issuer reference
|
||||
criteria.issuerRef?.let {
|
||||
val issuerRefs = (criteria.issuerRef as List<OpaqueBytes>).map { it.bytes }
|
||||
predicateSet.add(criteriaBuilder.and(vaultFungibleStates.get<ByteArray>("issuerRef").`in`(issuerRefs)))
|
||||
}
|
||||
|
||||
// participants
|
||||
criteria.participants?.let {
|
||||
val participants = criteria.participants as List<AbstractParty>
|
||||
val joinFungibleStateToParty = vaultFungibleStates.join<VaultSchemaV1.VaultFungibleStates, CommonSchemaV1.Party>("participants")
|
||||
val participantKeys = participants.map { it.nameOrNull().toString() }
|
||||
predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get<CommonSchemaV1.Party>("name").`in`(participantKeys)))
|
||||
criteriaQuery.distinct(true)
|
||||
}
|
||||
return predicateSet
|
||||
}
|
||||
|
||||
override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria) : Collection<Predicate> {
|
||||
log.trace { "Parsing LinearStateQueryCriteria: $criteria" }
|
||||
|
||||
val predicateSet = mutableSetOf<Predicate>()
|
||||
|
||||
val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java)
|
||||
rootEntities.putIfAbsent(VaultSchemaV1.VaultLinearStates::class.java, vaultLinearStates)
|
||||
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultLinearStates.get<PersistentStateRef>("stateRef"))
|
||||
joinPredicates.add(joinPredicate)
|
||||
|
||||
// linear ids
|
||||
criteria.linearId?.let {
|
||||
val uniqueIdentifiers = criteria.linearId as List<UniqueIdentifier>
|
||||
val externalIds = uniqueIdentifiers.mapNotNull { it.externalId }
|
||||
if (externalIds.isNotEmpty())
|
||||
predicateSet.add(criteriaBuilder.and(vaultLinearStates.get<String>("externalId").`in`(externalIds)))
|
||||
predicateSet.add(criteriaBuilder.and(vaultLinearStates.get<UUID>("uuid").`in`(uniqueIdentifiers.map { it.id })))
|
||||
}
|
||||
|
||||
// deal refs
|
||||
criteria.dealRef?.let {
|
||||
val dealRefs = criteria.dealRef as List<String>
|
||||
predicateSet.add(criteriaBuilder.and(vaultLinearStates.get<String>("dealReference").`in`(dealRefs)))
|
||||
}
|
||||
|
||||
// deal participants
|
||||
criteria.participants?.let {
|
||||
val participants = criteria.participants as List<AbstractParty>
|
||||
val joinLinearStateToParty = vaultLinearStates.join<VaultSchemaV1.VaultLinearStates, CommonSchemaV1.Party>("participants")
|
||||
val participantKeys = participants.map { it.nameOrNull().toString() }
|
||||
predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.get<CommonSchemaV1.Party>("name").`in`(participantKeys)))
|
||||
criteriaQuery.distinct(true)
|
||||
}
|
||||
return predicateSet
|
||||
}
|
||||
|
||||
override fun <L : PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> {
|
||||
log.trace { "Parsing VaultCustomQueryCriteria: $criteria" }
|
||||
|
||||
val predicateSet = mutableSetOf<Predicate>()
|
||||
val entityClass = resolveEnclosingObjectFromExpression(criteria.expression)
|
||||
|
||||
try {
|
||||
val entityRoot = criteriaQuery.from(entityClass)
|
||||
rootEntities.putIfAbsent(entityClass, entityRoot)
|
||||
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), entityRoot.get<PersistentStateRef>("stateRef"))
|
||||
joinPredicates.add(joinPredicate)
|
||||
|
||||
predicateSet.add(expressionToPredicate(entityRoot, criteria.expression))
|
||||
}
|
||||
catch (e: Exception) {
|
||||
e.message?.let { message ->
|
||||
if (message.contains("Not an entity"))
|
||||
throw VaultQueryException("""
|
||||
Please register the entity '${entityClass.name.substringBefore('$')}' class in your CorDapp's CordaPluginRegistry configuration (requiredSchemas attribute)
|
||||
and ensure you have declared (in supportedSchemas()) and mapped (in generateMappedObject()) the schema in the associated contract state's QueryableState interface implementation.
|
||||
See https://docs.corda.net/persistence.html?highlight=persistence for more information""")
|
||||
}
|
||||
throw VaultQueryException("Parsing error: ${e.message}")
|
||||
}
|
||||
return predicateSet
|
||||
}
|
||||
|
||||
override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
|
||||
log.trace { "Parsing OR QueryCriteria composition: $left OR $right" }
|
||||
|
||||
var predicateSet = mutableSetOf<Predicate>()
|
||||
val leftPredicates = parse(left)
|
||||
val rightPredicates = parse(right)
|
||||
|
||||
val orPredicate = criteriaBuilder.or(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray())
|
||||
predicateSet.add(orPredicate)
|
||||
|
||||
return predicateSet
|
||||
}
|
||||
|
||||
override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
|
||||
log.trace { "Parsing AND QueryCriteria composition: $left AND $right" }
|
||||
|
||||
var predicateSet = mutableSetOf<Predicate>()
|
||||
val leftPredicates = parse(left)
|
||||
val rightPredicates = parse(right)
|
||||
|
||||
val andPredicate = criteriaBuilder.and(criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()))
|
||||
predicateSet.add(andPredicate)
|
||||
|
||||
return predicateSet
|
||||
}
|
||||
|
||||
override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection<Predicate> {
|
||||
val predicateSet = criteria.visit(this)
|
||||
|
||||
sorting?.let {
|
||||
if (sorting.columns.isNotEmpty())
|
||||
parse(sorting)
|
||||
}
|
||||
|
||||
val selections = listOf(vaultStates).plus(rootEntities.map { it.value })
|
||||
criteriaQuery.multiselect(selections)
|
||||
val combinedPredicates = joinPredicates.plus(predicateSet)
|
||||
criteriaQuery.where(*combinedPredicates.toTypedArray())
|
||||
|
||||
return predicateSet
|
||||
}
|
||||
|
||||
private fun parse(sorting: Sort) {
|
||||
log.trace { "Parsing sorting specification: $sorting" }
|
||||
|
||||
var orderCriteria = mutableListOf<Order>()
|
||||
|
||||
sorting.columns.map { (sortAttribute, direction) ->
|
||||
val (entityStateClass, entityStateColumnName) =
|
||||
when(sortAttribute) {
|
||||
is SortAttribute.Standard -> parse(sortAttribute.attribute)
|
||||
is SortAttribute.Custom -> Pair(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName)
|
||||
}
|
||||
val sortEntityRoot =
|
||||
rootEntities.getOrElse(entityStateClass) {
|
||||
// scenario where sorting on attributes not parsed as criteria
|
||||
val entityRoot = criteriaQuery.from(entityStateClass)
|
||||
rootEntities.put(entityStateClass, entityRoot)
|
||||
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), entityRoot.get<PersistentStateRef>("stateRef"))
|
||||
joinPredicates.add(joinPredicate)
|
||||
entityRoot
|
||||
}
|
||||
when (direction) {
|
||||
Sort.Direction.ASC -> {
|
||||
orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get<String>(entityStateColumnName)))
|
||||
}
|
||||
Sort.Direction.DESC ->
|
||||
orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get<String>(entityStateColumnName)))
|
||||
}
|
||||
}
|
||||
if (orderCriteria.isNotEmpty()) {
|
||||
criteriaQuery.orderBy(orderCriteria)
|
||||
criteriaQuery.where(*joinPredicates.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun parse(sortAttribute: Sort.Attribute): Pair<Class<out PersistentState>, String> {
|
||||
val entityClassAndColumnName : Pair<Class<out PersistentState>, String> =
|
||||
when(sortAttribute) {
|
||||
is Sort.VaultStateAttribute -> {
|
||||
Pair(VaultSchemaV1.VaultStates::class.java, sortAttribute.columnName)
|
||||
}
|
||||
is Sort.LinearStateAttribute -> {
|
||||
Pair(VaultSchemaV1.VaultLinearStates::class.java, sortAttribute.columnName)
|
||||
}
|
||||
is Sort.FungibleStateAttribute -> {
|
||||
Pair(VaultSchemaV1.VaultFungibleStates::class.java, sortAttribute.columnName)
|
||||
}
|
||||
else -> throw VaultQueryException("Invalid sort attribute: $sortAttribute")
|
||||
}
|
||||
return entityClassAndColumnName
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.bufferUntilSubscribed
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultQueryException
|
||||
import net.corda.core.node.services.VaultQueryService
|
||||
import net.corda.core.node.services.vault.MAX_PAGE_SIZE
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.storageKryo
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1
|
||||
import net.corda.node.utilities.wrapWithDatabaseTransaction
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import rx.subjects.PublishSubject
|
||||
import java.lang.Exception
|
||||
import javax.persistence.EntityManager
|
||||
import javax.persistence.Tuple
|
||||
|
||||
|
||||
class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration,
|
||||
val updatesPublisher: PublishSubject<Vault.Update>) : SingletonSerializeAsToken(), VaultQueryService {
|
||||
|
||||
companion object {
|
||||
val log = loggerFor<HibernateVaultQueryImpl>()
|
||||
}
|
||||
|
||||
private val sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas()
|
||||
private val criteriaBuilder = sessionFactory.criteriaBuilder
|
||||
|
||||
@Throws(VaultQueryException::class)
|
||||
override fun <T : ContractState> _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class<out ContractState>): Vault.Page<T> {
|
||||
log.info("Vault Query for contract type: $contractType, criteria: $criteria, pagination: $paging, sorting: $sorting")
|
||||
|
||||
val session = sessionFactory.withOptions().
|
||||
connection(TransactionManager.current().connection).
|
||||
openSession()
|
||||
|
||||
session.use {
|
||||
val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java)
|
||||
val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
|
||||
val contractTypeMappings = resolveUniqueContractStateTypes(session)
|
||||
// TODO: revisit (use single instance of parser for all queries)
|
||||
val criteriaParser = HibernateQueryCriteriaParser(contractType, contractTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates)
|
||||
|
||||
try {
|
||||
// parse criteria and build where predicates
|
||||
criteriaParser.parse(criteria, sorting)
|
||||
|
||||
// prepare query for execution
|
||||
val query = session.createQuery(criteriaQuery)
|
||||
|
||||
// pagination
|
||||
if (paging.pageNumber < 0) throw VaultQueryException("Page specification: invalid page number ${paging.pageNumber} [page numbers start from 0]")
|
||||
if (paging.pageSize < 0 || paging.pageSize > MAX_PAGE_SIZE) throw VaultQueryException("Page specification: invalid page size ${paging.pageSize} [maximum page size is ${MAX_PAGE_SIZE}]")
|
||||
|
||||
// count total results available
|
||||
val countQuery = criteriaBuilder.createQuery(Long::class.java)
|
||||
countQuery.select(criteriaBuilder.count(countQuery.from(VaultSchemaV1.VaultStates::class.java)))
|
||||
val totalStates = session.createQuery(countQuery).singleResult.toInt()
|
||||
|
||||
if ((paging.pageNumber != 0) && (paging.pageSize * paging.pageNumber >= totalStates))
|
||||
throw VaultQueryException("Requested more results than available [${paging.pageSize} * ${paging.pageNumber} >= ${totalStates}]")
|
||||
|
||||
query.firstResult = paging.pageNumber * paging.pageSize
|
||||
query.maxResults = paging.pageSize
|
||||
|
||||
// execution
|
||||
val results = query.resultList
|
||||
val statesAndRefs: MutableList<StateAndRef<*>> = mutableListOf()
|
||||
val statesMeta: MutableList<Vault.StateMetadata> = mutableListOf()
|
||||
|
||||
results.asSequence()
|
||||
.forEach { it ->
|
||||
val it = it[0] as VaultSchemaV1.VaultStates
|
||||
val stateRef = StateRef(SecureHash.parse(it.stateRef!!.txId!!), it.stateRef!!.index!!)
|
||||
val state = it.contractState.deserialize<TransactionState<T>>(storageKryo())
|
||||
statesMeta.add(Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime))
|
||||
statesAndRefs.add(StateAndRef(state, stateRef))
|
||||
}
|
||||
|
||||
return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, pageable = paging, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates) as Vault.Page<T>
|
||||
|
||||
} catch (e: Exception) {
|
||||
log.error(e.message)
|
||||
throw e.cause ?: e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox ({ updatesPublisher })
|
||||
|
||||
@Throws(VaultQueryException::class)
|
||||
override fun <T : ContractState> _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class<out ContractState>): Vault.PageAndUpdates<T> {
|
||||
return mutex.locked {
|
||||
val snapshotResults = _queryBy<T>(criteria, paging, sorting, contractType)
|
||||
Vault.PageAndUpdates(snapshotResults,
|
||||
updatesPublisher.bufferUntilSubscribed()
|
||||
.filter { it.containsType(contractType, snapshotResults.stateTypes) } )
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintain a list of contract state interfaces to concrete types stored in the vault
|
||||
* for usage in generic queries of type queryBy<LinearState> or queryBy<FungibleState<*>>
|
||||
*/
|
||||
fun resolveUniqueContractStateTypes(session: EntityManager) : Map<String, List<String>> {
|
||||
val criteria = criteriaBuilder.createQuery(String::class.java)
|
||||
val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java)
|
||||
criteria.select(vaultStates.get("contractStateClassName")).distinct(true)
|
||||
val query = session.createQuery(criteria)
|
||||
val results = query.resultList
|
||||
val distinctTypes = results.map { it }
|
||||
|
||||
val contractInterfaceToConcreteTypes = mutableMapOf<String, MutableList<String>>()
|
||||
distinctTypes.forEach { it ->
|
||||
val concreteType = Class.forName(it) as Class<ContractState>
|
||||
val contractInterfaces = deriveContractInterfaces(concreteType)
|
||||
contractInterfaces.map {
|
||||
val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableListOf() })
|
||||
contractInterface.add(concreteType.name)
|
||||
}
|
||||
}
|
||||
return contractInterfaceToConcreteTypes
|
||||
}
|
||||
|
||||
private fun <T: ContractState> deriveContractInterfaces(clazz: Class<T>): Set<Class<T>> {
|
||||
val myInterfaces: MutableSet<Class<T>> = mutableSetOf()
|
||||
clazz.interfaces.forEach {
|
||||
if (!it.equals(ContractState::class.java)) {
|
||||
myInterfaces.add(it as Class<T>)
|
||||
myInterfaces.addAll(deriveContractInterfaces(it))
|
||||
}
|
||||
}
|
||||
return myInterfaces
|
||||
}
|
||||
}
|
@ -20,13 +20,7 @@ import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.StatesNotAvailableException
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.tee
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -35,7 +29,7 @@ import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.services.vault.schemas.*
|
||||
import net.corda.node.services.vault.schemas.requery.*
|
||||
import net.corda.node.utilities.bufferUntilDatabaseCommit
|
||||
import net.corda.node.utilities.wrapWithDatabaseTransaction
|
||||
import rx.Observable
|
||||
@ -173,6 +167,9 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
override val updates: Observable<Vault.Update>
|
||||
get() = mutex.locked { _updatesInDbTx }
|
||||
|
||||
override val updatesPublisher: PublishSubject<Vault.Update>
|
||||
get() = mutex.locked { _updatesPublisher }
|
||||
|
||||
override fun track(): Pair<Vault<ContractState>, Observable<Vault.Update>> {
|
||||
return mutex.locked {
|
||||
Pair(Vault(unconsumedStates<ContractState>()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
@ -222,26 +219,6 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
return stateAndRefs.associateBy({ it.ref }, { it.state })
|
||||
}
|
||||
|
||||
override fun <T : ContractState> queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page<T> {
|
||||
|
||||
TODO("Under construction")
|
||||
|
||||
// If [VaultQueryCriteria.PageSpecification] specified
|
||||
// must return (CloseableIterator) result.get().iterator(skip, take)
|
||||
// where
|
||||
// skip = Max[(pageNumber - 1),0] * pageSize
|
||||
// take = pageSize
|
||||
}
|
||||
|
||||
override fun <T : ContractState> trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates<T> {
|
||||
TODO("Under construction")
|
||||
|
||||
// return mutex.locked {
|
||||
// Vault.PageAndUpdates(queryBy(criteria),
|
||||
// _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
// }
|
||||
}
|
||||
|
||||
override fun notifyAll(txns: Iterable<WireTransaction>) {
|
||||
val ourKeys = services.keyManagementService.keys
|
||||
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn, ourKeys) }
|
||||
|
@ -0,0 +1,134 @@
|
||||
package net.corda.node.services.vault.schemas.jpa
|
||||
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* JPA representation of the core Vault Schema
|
||||
*/
|
||||
object VaultSchema
|
||||
|
||||
/**
|
||||
* First version of the Vault ORM schema
|
||||
*/
|
||||
object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1,
|
||||
mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, CommonSchemaV1.Party::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "vault_states",
|
||||
indexes = arrayOf(Index(name = "state_status_idx", columnList = "state_status")))
|
||||
class VaultStates(
|
||||
/** refers to the notary a state is attached to */
|
||||
@Column(name = "notary_name")
|
||||
var notaryName: String,
|
||||
|
||||
@Column(name = "notary_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?
|
||||
var notaryKey: String,
|
||||
|
||||
/** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */
|
||||
@Column(name = "contract_state_class_name")
|
||||
var contractStateClassName: String,
|
||||
|
||||
/** refers to serialized transaction Contract State */
|
||||
// TODO: define contract state size maximum size and adjust length accordingly
|
||||
@Column(name = "contract_state", length = 100000)
|
||||
var contractState: ByteArray,
|
||||
|
||||
/** state lifecycle: unconsumed, consumed */
|
||||
@Column(name = "state_status")
|
||||
var stateStatus: Vault.StateStatus,
|
||||
|
||||
/** refers to timestamp recorded upon entering UNCONSUMED state */
|
||||
@Column(name = "recorded_timestamp")
|
||||
var recordedTime: Instant,
|
||||
|
||||
/** refers to timestamp recorded upon entering CONSUMED state */
|
||||
@Column(name = "consumed_timestamp", nullable = true)
|
||||
var consumedTime: Instant?,
|
||||
|
||||
/** used to denote a state has been soft locked (to prevent double spend)
|
||||
* will contain a temporary unique [UUID] obtained from a flow session */
|
||||
@Column(name = "lock_id", nullable = true)
|
||||
var lockId: String,
|
||||
|
||||
/** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
|
||||
@Column(name = "lock_timestamp", nullable = true)
|
||||
var lockUpdateTime: Instant?
|
||||
) : PersistentState()
|
||||
|
||||
@Entity
|
||||
@Table(name = "vault_linear_states",
|
||||
indexes = arrayOf(Index(name = "external_id_index", columnList = "external_id"),
|
||||
Index(name = "uuid_index", columnList = "uuid"),
|
||||
Index(name = "deal_reference_index", columnList = "deal_reference")))
|
||||
class VaultLinearStates(
|
||||
/** [ContractState] attributes */
|
||||
@OneToMany(cascade = arrayOf(CascadeType.ALL))
|
||||
var participants: Set<CommonSchemaV1.Party>,
|
||||
|
||||
/**
|
||||
* Represents a [LinearState] [UniqueIdentifier]
|
||||
*/
|
||||
@Column(name = "external_id")
|
||||
var externalId: String?,
|
||||
|
||||
@Column(name = "uuid", nullable = false)
|
||||
var uuid: UUID,
|
||||
|
||||
// TODO: DealState to be deprecated (collapsed into LinearState)
|
||||
|
||||
/** Deal State attributes **/
|
||||
@Column(name = "deal_reference")
|
||||
var dealReference: String
|
||||
) : PersistentState() {
|
||||
constructor(uid: UniqueIdentifier, _dealReference: String, _participants: List<AbstractParty>) :
|
||||
this(externalId = uid.externalId,
|
||||
uuid = uid.id,
|
||||
dealReference = _dealReference,
|
||||
participants = _participants.map{ CommonSchemaV1.Party(it) }.toSet() )
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "vault_fungible_states")
|
||||
class VaultFungibleStates(
|
||||
/** [ContractState] attributes */
|
||||
@OneToMany(cascade = arrayOf(CascadeType.ALL))
|
||||
var participants: Set<CommonSchemaV1.Party>,
|
||||
|
||||
/** [OwnableState] attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var owner: CommonSchemaV1.Party,
|
||||
|
||||
/** [FungibleAsset] attributes
|
||||
*
|
||||
* Note: the underlying Product being issued must be modelled into the
|
||||
* custom contract itself (eg. see currency in Cash contract state)
|
||||
*/
|
||||
|
||||
/** Amount attributes */
|
||||
@Column(name = "quantity")
|
||||
var quantity: Long,
|
||||
|
||||
/** Issuer attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var issuerParty: CommonSchemaV1.Party,
|
||||
|
||||
@Column(name = "issuer_reference")
|
||||
var issuerRef: ByteArray
|
||||
) : PersistentState() {
|
||||
constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List<AbstractParty>) :
|
||||
this(owner = CommonSchemaV1.Party(_owner),
|
||||
quantity = _quantity,
|
||||
issuerParty = CommonSchemaV1.Party(_issuerParty),
|
||||
issuerRef = _issuerRef.bytes,
|
||||
participants = _participants.map { CommonSchemaV1.Party(it) }.toSet())
|
||||
}
|
||||
}
|
@ -1,72 +1,71 @@
|
||||
package net.corda.node.services.vault;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import kotlin.Pair;
|
||||
import net.corda.contracts.DealState;
|
||||
import net.corda.contracts.asset.Cash;
|
||||
import com.google.common.collect.*;
|
||||
import kotlin.*;
|
||||
import net.corda.contracts.*;
|
||||
import net.corda.contracts.asset.*;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.node.services.Vault;
|
||||
import net.corda.core.node.services.VaultService;
|
||||
import net.corda.core.node.services.vault.PageSpecification;
|
||||
import net.corda.core.node.services.vault.QueryCriteria;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||
import net.corda.core.node.services.vault.Sort;
|
||||
import net.corda.core.serialization.OpaqueBytes;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.WireTransaction;
|
||||
import net.corda.node.services.vault.schemas.VaultLinearStateEntity;
|
||||
import net.corda.testing.node.MockServices;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.exposed.sql.Database;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import net.corda.core.crypto.*;
|
||||
import net.corda.core.identity.*;
|
||||
import net.corda.core.node.services.*;
|
||||
import net.corda.core.node.services.vault.*;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.*;
|
||||
import net.corda.core.schemas.*;
|
||||
import net.corda.core.serialization.*;
|
||||
import net.corda.core.transactions.*;
|
||||
import net.corda.node.services.database.*;
|
||||
import net.corda.node.services.schema.*;
|
||||
import net.corda.schemas.*;
|
||||
import net.corda.testing.node.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
import org.jetbrains.exposed.sql.*;
|
||||
import org.junit.*;
|
||||
import rx.Observable;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import java.util.stream.*;
|
||||
|
||||
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER;
|
||||
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY;
|
||||
import static net.corda.contracts.asset.CashKt.*;
|
||||
import static net.corda.contracts.testing.VaultFiller.*;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaKt.and;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.getMAX_PAGE_SIZE;
|
||||
import static net.corda.core.utilities.TestConstants.getDUMMY_NOTARY;
|
||||
import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaKt.*;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.*;
|
||||
import static net.corda.core.utilities.TestConstants.*;
|
||||
import static net.corda.node.utilities.DatabaseSupportKt.*;
|
||||
import static net.corda.node.utilities.DatabaseSupportKt.transaction;
|
||||
import static net.corda.testing.CoreTestUtils.getMEGA_CORP;
|
||||
import static net.corda.testing.node.MockServicesKt.makeTestDataSourceProperties;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static net.corda.testing.CoreTestUtils.*;
|
||||
import static net.corda.testing.node.MockServicesKt.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
@Ignore
|
||||
public class VaultQueryJavaTests {
|
||||
|
||||
private MockServices services;
|
||||
private VaultService vaultSvc;
|
||||
VaultService vaultSvc;
|
||||
private VaultQueryService vaultQuerySvc;
|
||||
private Closeable dataSource;
|
||||
private Database database;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString());
|
||||
Pair<Closeable, Database> dataSourceAndDatabase = configureDatabase(dataSourceProps);
|
||||
dataSource = dataSourceAndDatabase.getFirst();
|
||||
database = dataSourceAndDatabase.getSecond();
|
||||
|
||||
transaction(database, statement -> services = new MockServices() {
|
||||
Set<MappedSchema> customSchemas = new HashSet<>(Arrays.asList(DummyLinearStateSchemaV1.INSTANCE));
|
||||
HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas));
|
||||
transaction(database,
|
||||
statement -> { services = new MockServices(getMEGA_CORP_KEY()) {
|
||||
@NotNull
|
||||
@Override
|
||||
public VaultService getVaultService() {
|
||||
return makeVaultService(dataSourceProps);
|
||||
return makeVaultService(dataSourceProps, hibernateConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VaultQueryService getVaultQueryService() {
|
||||
return new HibernateVaultQueryImpl(hibernateConfig, getVaultService().getUpdatesPublisher());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -78,9 +77,12 @@ public class VaultQueryJavaTests {
|
||||
Stream<WireTransaction> wtxn = StreamSupport.stream(txs.spliterator(), false).map(txn -> txn.getTx());
|
||||
getVaultService().notifyAll(wtxn.collect(Collectors.toList()));
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
vaultSvc = services.getVaultService();
|
||||
vaultQuerySvc = services.getVaultQueryService();
|
||||
|
||||
return services;
|
||||
});
|
||||
}
|
||||
|
||||
@After
|
||||
@ -97,8 +99,27 @@ public class VaultQueryJavaTests {
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void consumedStates() {
|
||||
public void unconsumedLinearStates() throws VaultQueryException {
|
||||
transaction(database, tx -> {
|
||||
|
||||
fillWithSomeTestLinearStates(services, 3);
|
||||
|
||||
// DOCSTART VaultJavaQueryExample0
|
||||
Vault.Page<LinearState> results = vaultQuerySvc.queryBy(LinearState.class);
|
||||
// DOCEND VaultJavaQueryExample0
|
||||
|
||||
assertThat(results.getStates()).hasSize(3);
|
||||
|
||||
return tx;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void consumedCashStates() {
|
||||
transaction(database, tx -> {
|
||||
|
||||
Amount<Currency> amount = new Amount<>(100, Currency.getInstance("USD"));
|
||||
|
||||
fillWithSomeTestCash(services,
|
||||
new Amount<>(100, Currency.getInstance("USD")),
|
||||
getDUMMY_NOTARY(),
|
||||
@ -110,13 +131,11 @@ public class VaultQueryJavaTests {
|
||||
getDUMMY_CASH_ISSUER(),
|
||||
getDUMMY_CASH_ISSUER_KEY() );
|
||||
|
||||
// DOCSTART VaultJavaQueryExample1
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class));
|
||||
Vault.StateStatus status = Vault.StateStatus.CONSUMED;
|
||||
consumeCash(services, amount);
|
||||
|
||||
VaultQueryCriteria criteria = new VaultQueryCriteria(status, null, contractStateTypes);
|
||||
Vault.Page<ContractState> results = vaultSvc.queryBy(criteria);
|
||||
// DOCSTART VaultJavaQueryExample1
|
||||
VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED);
|
||||
Vault.Page<Cash.State> results = vaultQuerySvc.queryBy(Cash.State.class, criteria);
|
||||
// DOCEND VaultJavaQueryExample1
|
||||
|
||||
assertThat(results.getStates()).hasSize(3);
|
||||
@ -126,32 +145,38 @@ public class VaultQueryJavaTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void consumedDealStatesPagedSorted() {
|
||||
public void consumedDealStatesPagedSorted() throws VaultQueryException {
|
||||
transaction(database, tx -> {
|
||||
|
||||
UniqueIdentifier uid = new UniqueIdentifier();
|
||||
fillWithSomeTestLinearStates(services, 10, uid);
|
||||
Vault<LinearState> states = fillWithSomeTestLinearStates(services, 10, null);
|
||||
StateAndRef<LinearState> linearState = states.getStates().iterator().next();
|
||||
UniqueIdentifier uid = linearState.component1().getData().getLinearId();
|
||||
|
||||
List<String> dealIds = Arrays.asList("123", "456", "789");
|
||||
fillWithSomeTestDeals(services, dealIds);
|
||||
Vault<DealState> dealStates = fillWithSomeTestDeals(services, dealIds);
|
||||
|
||||
// consume states
|
||||
consumeDeals(services, (List<? extends StateAndRef<? extends DealState>>) dealStates.getStates());
|
||||
consumeLinearStates(services, Arrays.asList(linearState));
|
||||
|
||||
// DOCSTART VaultJavaQueryExample2
|
||||
Vault.StateStatus status = Vault.StateStatus.CONSUMED;
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class));
|
||||
Set<Class<LinearState>> contractStateTypes = new HashSet(Collections.singletonList(LinearState.class));
|
||||
|
||||
QueryCriteria vaultCriteria = new VaultQueryCriteria(status, null, contractStateTypes);
|
||||
QueryCriteria vaultCriteria = new VaultQueryCriteria(status, contractStateTypes);
|
||||
|
||||
List<UniqueIdentifier> linearIds = Arrays.asList(uid);
|
||||
List<X500Name> dealPartyNames = Arrays.asList(getMEGA_CORP().getName());
|
||||
QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(linearIds, false, dealIds, dealPartyNames);
|
||||
QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds);
|
||||
QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds);
|
||||
|
||||
QueryCriteria compositeCriteria = and(dealCriteriaAll, vaultCriteria);
|
||||
QueryCriteria compositeCriteria1 = or(dealCriteriaAll, linearCriteriaAll);
|
||||
QueryCriteria compositeCriteria2 = and(vaultCriteria, compositeCriteria1);
|
||||
|
||||
PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE());
|
||||
Sort.SortColumn sortByUid = new Sort.SortColumn(VaultLinearStateEntity.UUID.getName(), Sort.Direction.DESC, Sort.NullHandling.NULLS_LAST);
|
||||
Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC);
|
||||
Sort sorting = new Sort(ImmutableSet.of(sortByUid));
|
||||
Vault.Page<ContractState> results = vaultSvc.queryBy(compositeCriteria, pageSpec, sorting);
|
||||
Vault.Page<LinearState> results = vaultQuerySvc.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting);
|
||||
// DOCEND VaultJavaQueryExample2
|
||||
|
||||
assertThat(results.getStates()).hasSize(4);
|
||||
@ -160,13 +185,52 @@ public class VaultQueryJavaTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customQueryForCashStatesWithAmountOfCurrencyGreaterOrEqualThanQuantity() {
|
||||
transaction(database, tx -> {
|
||||
|
||||
Amount<Currency> pounds = new Amount<>(100, Currency.getInstance("GBP"));
|
||||
Amount<Currency> dollars100 = new Amount<>(100, Currency.getInstance("USD"));
|
||||
Amount<Currency> dollars10 = new Amount<>(10, Currency.getInstance("USD"));
|
||||
Amount<Currency> dollars1 = new Amount<>(1, Currency.getInstance("USD"));
|
||||
|
||||
fillWithSomeTestCash(services, pounds, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
|
||||
fillWithSomeTestCash(services, dollars100, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
|
||||
fillWithSomeTestCash(services, dollars10, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
|
||||
fillWithSomeTestCash(services, dollars1, getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
|
||||
|
||||
try {
|
||||
// DOCSTART VaultJavaQueryExample3
|
||||
QueryCriteria generalCriteria = new VaultQueryCriteria(Vault.StateStatus.ALL);
|
||||
|
||||
Field attributeCurrency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency");
|
||||
Field attributeQuantity = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
|
||||
|
||||
CriteriaExpression currencyIndex = Builder.INSTANCE.equal(attributeCurrency, "USD");
|
||||
CriteriaExpression quantityIndex = Builder.INSTANCE.greaterThanOrEqual(attributeQuantity, 10L);
|
||||
|
||||
QueryCriteria customCriteria2 = new VaultCustomQueryCriteria(quantityIndex);
|
||||
QueryCriteria customCriteria1 = new VaultCustomQueryCriteria(currencyIndex);
|
||||
|
||||
|
||||
QueryCriteria criteria = QueryCriteriaKt.and(QueryCriteriaKt.and(generalCriteria, customCriteria1), customCriteria2);
|
||||
Vault.Page<ContractState> results = vaultQuerySvc.queryBy(Cash.State.class, criteria);
|
||||
// DOCEND VaultJavaQueryExample3
|
||||
|
||||
assertThat(results.getStates()).hasSize(2);
|
||||
} catch (NoSuchFieldException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return tx;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic trackBy() tests
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void trackCashStates() {
|
||||
|
||||
transaction(database, tx -> {
|
||||
fillWithSomeTestCash(services,
|
||||
new Amount<>(100, Currency.getInstance("USD")),
|
||||
@ -179,17 +243,17 @@ public class VaultQueryJavaTests {
|
||||
getDUMMY_CASH_ISSUER(),
|
||||
getDUMMY_CASH_ISSUER_KEY() );
|
||||
|
||||
// DOCSTART VaultJavaQueryExample1
|
||||
// DOCSTART VaultJavaQueryExample4
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class));
|
||||
|
||||
VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, contractStateTypes);
|
||||
Vault.PageAndUpdates<ContractState> results = vaultSvc.trackBy(criteria);
|
||||
VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes);
|
||||
Vault.PageAndUpdates<ContractState> results = vaultQuerySvc.trackBy(ContractState.class, criteria);
|
||||
|
||||
Vault.Page<ContractState> snapshot = results.getCurrent();
|
||||
Observable<Vault.Update> updates = results.getFuture();
|
||||
|
||||
// DOCEND VaultJavaQueryExample1
|
||||
// DOCEND VaultJavaQueryExample4
|
||||
assertThat(snapshot.getStates()).hasSize(3);
|
||||
|
||||
return tx;
|
||||
@ -200,31 +264,36 @@ public class VaultQueryJavaTests {
|
||||
public void trackDealStatesPagedSorted() {
|
||||
transaction(database, tx -> {
|
||||
|
||||
UniqueIdentifier uid = new UniqueIdentifier();
|
||||
fillWithSomeTestLinearStates(services, 10, uid);
|
||||
Vault<LinearState> states = fillWithSomeTestLinearStates(services, 10, null);
|
||||
UniqueIdentifier uid = states.getStates().iterator().next().component1().getData().getLinearId();
|
||||
|
||||
List<String> dealIds = Arrays.asList("123", "456", "789");
|
||||
fillWithSomeTestDeals(services, dealIds);
|
||||
|
||||
// DOCSTART VaultJavaQueryExample2
|
||||
// DOCSTART VaultJavaQueryExample5
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Collections.singletonList(DealState.class));
|
||||
QueryCriteria vaultCriteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, contractStateTypes);
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Arrays.asList(DealState.class, LinearState.class));
|
||||
QueryCriteria vaultCriteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes);
|
||||
|
||||
List<UniqueIdentifier> linearIds = Arrays.asList(uid);
|
||||
List<X500Name> dealPartyNames = Arrays.asList(getMEGA_CORP().getName());
|
||||
QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(linearIds, false, dealIds, dealPartyNames);
|
||||
List<AbstractParty> dealParty = Arrays.asList(getMEGA_CORP());
|
||||
QueryCriteria dealCriteria = new LinearStateQueryCriteria(dealParty, null, dealIds);
|
||||
|
||||
QueryCriteria compositeCriteria = and(dealCriteriaAll, vaultCriteria);
|
||||
QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, null);
|
||||
|
||||
|
||||
QueryCriteria dealOrLinearIdCriteria = or(dealCriteria, linearCriteria);
|
||||
|
||||
QueryCriteria compositeCriteria = and(dealOrLinearIdCriteria, vaultCriteria);
|
||||
|
||||
PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE());
|
||||
Sort.SortColumn sortByUid = new Sort.SortColumn(VaultLinearStateEntity.UUID.getName(), Sort.Direction.DESC, Sort.NullHandling.NULLS_LAST);
|
||||
Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC);
|
||||
Sort sorting = new Sort(ImmutableSet.of(sortByUid));
|
||||
Vault.PageAndUpdates<ContractState> results = vaultSvc.trackBy(compositeCriteria, pageSpec, sorting);
|
||||
Vault.PageAndUpdates<ContractState> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting);
|
||||
|
||||
Vault.Page<ContractState> snapshot = results.getCurrent();
|
||||
Observable<Vault.Update> updates = results.getFuture();
|
||||
// DOCEND VaultJavaQueryExample2
|
||||
// DOCEND VaultJavaQueryExample5
|
||||
|
||||
assertThat(snapshot.getStates()).hasSize(4);
|
||||
|
||||
@ -239,6 +308,7 @@ public class VaultQueryJavaTests {
|
||||
@Test
|
||||
public void consumedStatesDeprecated() {
|
||||
transaction(database, tx -> {
|
||||
Amount<Currency> amount = new Amount<>(100, Currency.getInstance("USD"));
|
||||
fillWithSomeTestCash(services,
|
||||
new Amount<>(100, Currency.getInstance("USD")),
|
||||
getDUMMY_NOTARY(),
|
||||
@ -250,6 +320,8 @@ public class VaultQueryJavaTests {
|
||||
getDUMMY_CASH_ISSUER(),
|
||||
getDUMMY_CASH_ISSUER_KEY() );
|
||||
|
||||
consumeCash(services, amount);
|
||||
|
||||
// DOCSTART VaultDeprecatedJavaQueryExample1
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class));
|
||||
@ -269,24 +341,21 @@ public class VaultQueryJavaTests {
|
||||
public void consumedStatesForLinearIdDeprecated() {
|
||||
transaction(database, tx -> {
|
||||
|
||||
UniqueIdentifier trackUid = new UniqueIdentifier();
|
||||
fillWithSomeTestLinearStates(services, 1, trackUid);
|
||||
fillWithSomeTestLinearStates(services, 4, new UniqueIdentifier());
|
||||
Vault<LinearState> linearStates = fillWithSomeTestLinearStates(services, 4,null);
|
||||
UniqueIdentifier trackUid = linearStates.getStates().iterator().next().component1().getData().getLinearId();
|
||||
|
||||
// DOCSTART VaultDeprecatedJavaQueryExample2
|
||||
consumeLinearStates(services, (List<? extends StateAndRef<? extends LinearState>>) linearStates.getStates());
|
||||
|
||||
// DOCSTART VaultDeprecatedJavaQueryExample0
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Collections.singletonList(LinearState.class));
|
||||
Set<Class<LinearState>> contractStateTypes = new HashSet(Collections.singletonList(DummyLinearContract.State.class));
|
||||
EnumSet<Vault.StateStatus> status = EnumSet.of(Vault.StateStatus.CONSUMED);
|
||||
|
||||
// WARNING! unfortunately cannot use inlined reified Kotlin extension methods.
|
||||
Iterable<StateAndRef<ContractState>> results = vaultSvc.states(contractStateTypes, status, true);
|
||||
|
||||
Stream<StateAndRef<ContractState>> trackedLinearState = StreamSupport.stream(results.spliterator(), false).filter(
|
||||
state -> ((LinearState) state.component1().getData()).getLinearId() == trackUid);
|
||||
// DOCEND VaultDeprecatedJavaQueryExample2
|
||||
Iterable<StateAndRef<LinearState>> results = vaultSvc.states(contractStateTypes, status, true);
|
||||
// DOCEND VaultDeprecatedJavaQueryExample0
|
||||
|
||||
assertThat(results).hasSize(4);
|
||||
assertThat(trackedLinearState).hasSize(1);
|
||||
|
||||
return tx;
|
||||
});
|
||||
|
@ -53,7 +53,8 @@ class CordaRPCOpsImplTest {
|
||||
lateinit var rpc: CordaRPCOpsImpl
|
||||
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update> // TODO: deprecated
|
||||
lateinit var vaultTrackCash: Observable<Vault.Update>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@ -71,6 +72,7 @@ class CordaRPCOpsImplTest {
|
||||
stateMachineUpdates = rpc.stateMachinesAndUpdates().second
|
||||
transactions = rpc.verifiedTransactions().second
|
||||
vaultUpdates = rpc.vaultAndUpdates().second
|
||||
vaultTrackCash = rpc.vaultTrackBy<Cash.State>().future
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,12 +114,20 @@ class CordaRPCOpsImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: deprecated
|
||||
vaultUpdates.expectEvents {
|
||||
expect { update ->
|
||||
val actual = update.produced.single().state.data
|
||||
assertEquals(expectedState, actual)
|
||||
}
|
||||
}
|
||||
|
||||
vaultTrackCash.expectEvents {
|
||||
expect { update ->
|
||||
val actual = update.produced.single().state.data
|
||||
assertEquals(expectedState, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -180,6 +190,7 @@ class CordaRPCOpsImplTest {
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: deprecated
|
||||
vaultUpdates.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
@ -194,6 +205,21 @@ class CordaRPCOpsImplTest {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
vaultTrackCash.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { update ->
|
||||
require(update.consumed.isEmpty()) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
},
|
||||
// MOVE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 1) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -12,7 +12,7 @@ import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.persistence.schemas.AttachmentEntity
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.node.utilities.transaction
|
||||
|
@ -22,6 +22,7 @@ import java.time.Clock
|
||||
|
||||
open class MockServiceHubInternal(
|
||||
val customVault: VaultService? = null,
|
||||
val customVaultQuery: VaultQueryService? = null,
|
||||
val keyManagement: KeyManagementService? = null,
|
||||
val network: MessagingService? = null,
|
||||
val identity: IdentityService? = MOCK_IDENTITY_SERVICE,
|
||||
@ -32,6 +33,8 @@ open class MockServiceHubInternal(
|
||||
val schemas: SchemaService? = NodeSchemaService(),
|
||||
val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2)
|
||||
) : ServiceHubInternal() {
|
||||
override val vaultQueryService: VaultQueryService
|
||||
get() = customVaultQuery ?: throw UnsupportedOperationException()
|
||||
override val transactionVerifierService: TransactionVerifierService
|
||||
get() = customTransactionVerifierService ?: throw UnsupportedOperationException()
|
||||
override val vaultService: VaultService
|
||||
|
@ -0,0 +1,698 @@
|
||||
package net.corda.node.services.database
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DummyFungibleContract
|
||||
import net.corda.contracts.testing.consumeCash
|
||||
import net.corda.contracts.testing.fillWithSomeTestCash
|
||||
import net.corda.contracts.testing.fillWithSomeTestDeals
|
||||
import net.corda.contracts.testing.fillWithSomeTestLinearStates
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.schemas.DummyLinearStateSchemaV1
|
||||
import net.corda.core.schemas.DummyLinearStateSchemaV2
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.storageKryo
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ALICE
|
||||
import net.corda.core.utilities.BOB
|
||||
import net.corda.core.utilities.BOB_KEY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.node.services.schema.HibernateObserver
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.services.vault.schemas.jpa.CommonSchemaV1
|
||||
import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.node.utilities.transaction
|
||||
import net.corda.schemas.CashSchemaV1
|
||||
import net.corda.schemas.SampleCashSchemaV2
|
||||
import net.corda.schemas.SampleCashSchemaV3
|
||||
import net.corda.testing.BOB_PUBKEY
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.hibernate.SessionFactory
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.Closeable
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.EntityManager
|
||||
import javax.persistence.Tuple
|
||||
import javax.persistence.criteria.CriteriaBuilder
|
||||
|
||||
class HibernateConfigurationTest {
|
||||
|
||||
lateinit var services: MockServices
|
||||
lateinit var dataSource: Closeable
|
||||
lateinit var database: Database
|
||||
val vault: VaultService get() = services.vaultService
|
||||
|
||||
// Hibernate configuration objects
|
||||
lateinit var hibernateConfig: HibernateConfiguration
|
||||
lateinit var hibernatePersister: HibernateObserver
|
||||
lateinit var sessionFactory: SessionFactory
|
||||
lateinit var entityManager: EntityManager
|
||||
lateinit var criteriaBuilder: CriteriaBuilder
|
||||
|
||||
// test States
|
||||
lateinit var cashStates: List<StateAndRef<Cash.State>>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
val dataSourceAndDatabase = configureDatabase(dataSourceProps)
|
||||
val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3)
|
||||
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
database.transaction {
|
||||
|
||||
hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas))
|
||||
|
||||
services = object : MockServices(BOB_KEY) {
|
||||
override val vaultService: VaultService get() {
|
||||
val vaultService = NodeVaultService(this, dataSourceProps)
|
||||
hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig)
|
||||
return vaultService
|
||||
}
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
storageService.validatedTransactions.addTransaction(stx)
|
||||
}
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
}
|
||||
}
|
||||
setUpDb()
|
||||
|
||||
sessionFactory = hibernateConfig.sessionFactoryForSchemas(*customSchemas.toTypedArray())
|
||||
entityManager = sessionFactory.createEntityManager()
|
||||
criteriaBuilder = sessionFactory.criteriaBuilder
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
dataSource.close()
|
||||
}
|
||||
|
||||
private fun setUpDb() {
|
||||
database.transaction {
|
||||
cashStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)).states.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `count rows`() {
|
||||
// structure query
|
||||
val countQuery = criteriaBuilder.createQuery(Long::class.java)
|
||||
countQuery.select(criteriaBuilder.count(countQuery.from(VaultSchemaV1.VaultStates::class.java)))
|
||||
|
||||
// execute query
|
||||
val countResult = entityManager.createQuery(countQuery).singleResult
|
||||
|
||||
assertThat(countResult).isEqualTo(10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consumed states`() {
|
||||
database.transaction {
|
||||
services.consumeCash(50.DOLLARS)
|
||||
}
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
criteriaQuery.where(criteriaBuilder.equal(
|
||||
vaultStates.get<Vault.StateStatus>("stateStatus"), Vault.StateStatus.CONSUMED))
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults.size).isEqualTo(6)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `select by composite primary key`() {
|
||||
val issuedStates =
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(8)
|
||||
services.fillWithSomeTestLinearStates(2)
|
||||
}
|
||||
val persistentStateRefs = issuedStates.states.map { PersistentStateRef(it.ref) }.toList()
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val compositeKey = vaultStates.get<PersistentStateRef>("stateRef")
|
||||
criteriaQuery.where(criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)))
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
|
||||
assertThat(queryResults).hasSize(2)
|
||||
assertThat(queryResults.first().stateRef?.txId).isEqualTo(issuedStates.states.first().ref.txhash.toString())
|
||||
assertThat(queryResults.first().stateRef?.index).isEqualTo(issuedStates.states.first().ref.index)
|
||||
assertThat(queryResults.last().stateRef?.txId).isEqualTo(issuedStates.states.last().ref.txhash.toString())
|
||||
assertThat(queryResults.last().stateRef?.index).isEqualTo(issuedStates.states.last().ref.index)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `distinct contract types`() {
|
||||
database.transaction {
|
||||
// add 2 more contract types
|
||||
services.fillWithSomeTestLinearStates(10)
|
||||
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
|
||||
}
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(String::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
criteriaQuery.select(vaultStates.get("contractStateClassName")).distinct(true)
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
Assertions.assertThat(queryResults.size).isEqualTo(3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with sorting`() {
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
|
||||
// order by DESC
|
||||
criteriaQuery.orderBy(criteriaBuilder.desc(vaultStates.get<Instant>("recordedTime")))
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
queryResults.map { println(it.recordedTime) }
|
||||
|
||||
// order by ASC
|
||||
criteriaQuery.orderBy(criteriaBuilder.asc(vaultStates.get<Instant>("recordedTime")))
|
||||
val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList
|
||||
queryResultsAsc.map { println(it.recordedTime) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with pagination`() {
|
||||
// add 100 additional cash entries
|
||||
database.transaction {
|
||||
services.fillWithSomeTestCash(1000.POUNDS, DUMMY_NOTARY, 100, 100, Random(0L))
|
||||
}
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
|
||||
// set pagination
|
||||
val query = entityManager.createQuery(criteriaQuery)
|
||||
query.firstResult = 10
|
||||
query.maxResults = 15
|
||||
|
||||
// execute query
|
||||
val queryResults = query.resultList
|
||||
Assertions.assertThat(queryResults.size).isEqualTo(15)
|
||||
|
||||
// try towards end
|
||||
query.firstResult = 100
|
||||
query.maxResults = 15
|
||||
|
||||
val lastQueryResults = query.resultList
|
||||
|
||||
Assertions.assertThat(lastQueryResults.size).isEqualTo(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* VaultLinearState is a concrete table, extendible by any Contract extending a LinearState
|
||||
*/
|
||||
@Test
|
||||
fun `select by composite primary key on LinearStates`() {
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(10)
|
||||
}
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java)
|
||||
|
||||
criteriaQuery.select(vaultStates)
|
||||
criteriaQuery.where(criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultLinearStates.get<PersistentStateRef>("stateRef")))
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults).hasSize(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* VaultFungibleState is an abstract entity, which should be extended any Contract extending a FungibleAsset
|
||||
*/
|
||||
|
||||
/**
|
||||
* CashSchemaV1 = original Cash schema (extending PersistentState)
|
||||
*/
|
||||
@Test
|
||||
fun `count CashStates`() {
|
||||
// structure query
|
||||
val countQuery = criteriaBuilder.createQuery(Long::class.java)
|
||||
countQuery.select(criteriaBuilder.count(countQuery.from(CashSchemaV1.PersistentCashState::class.java)))
|
||||
|
||||
// execute query
|
||||
val countResult = entityManager.createQuery(countQuery).singleResult
|
||||
|
||||
assertThat(countResult).isEqualTo(10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `select by composite primary key on CashStates`() {
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
vaultStates.join<VaultSchemaV1.VaultStates, CashSchemaV1.PersistentCashState>("stateRef")
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults).hasSize(10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `select and join by composite primary key on CashStates`() {
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(5)
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultCashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java)
|
||||
|
||||
criteriaQuery.select(vaultStates)
|
||||
criteriaQuery.where(criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultCashStates.get<PersistentStateRef>("stateRef")))
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults).hasSize(10)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CashSchemaV2 = optimised Cash schema (extending FungibleState)
|
||||
*/
|
||||
@Test
|
||||
fun `count CashStates in V2`() {
|
||||
database.transaction {
|
||||
// persist cash states explicitly with V2 schema
|
||||
cashStates.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV2)
|
||||
}
|
||||
}
|
||||
|
||||
// structure query
|
||||
val countQuery = criteriaBuilder.createQuery(Long::class.java)
|
||||
countQuery.select(criteriaBuilder.count(countQuery.from(SampleCashSchemaV2.PersistentCashState::class.java)))
|
||||
|
||||
// execute query
|
||||
val countResult = entityManager.createQuery(countQuery).singleResult
|
||||
|
||||
assertThat(countResult).isEqualTo(10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `select by composite primary key on CashStates in V2`() {
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(5)
|
||||
|
||||
// persist cash states explicitly with V2 schema
|
||||
cashStates.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV2)
|
||||
}
|
||||
}
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultCashStates = criteriaQuery.from(SampleCashSchemaV2.PersistentCashState::class.java)
|
||||
|
||||
criteriaQuery.select(vaultStates)
|
||||
criteriaQuery.where(criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultCashStates.get<PersistentStateRef>("stateRef")))
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults).hasSize(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 3-way join between:
|
||||
* - VaultStates
|
||||
* - VaultLinearStates
|
||||
* - a concrete LinearState implementation (eg. DummyLinearState)
|
||||
*/
|
||||
|
||||
/**
|
||||
* DummyLinearStateV1 = original DummyLinearState schema (extending PersistentState)
|
||||
*/
|
||||
@Test
|
||||
fun `select by composite primary between VaultStates, VaultLinearStates and DummyLinearStates`() {
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(8)
|
||||
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
|
||||
services.fillWithSomeTestLinearStates(2)
|
||||
}
|
||||
|
||||
val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1)
|
||||
val criteriaBuilder = sessionFactory.criteriaBuilder
|
||||
val entityManager = sessionFactory.createEntityManager()
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java)
|
||||
val dummyLinearStates = criteriaQuery.from(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java)
|
||||
|
||||
criteriaQuery.select(vaultStates)
|
||||
val joinPredicate1 = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultLinearStates.get<PersistentStateRef>("stateRef"))
|
||||
val joinPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), dummyLinearStates.get<PersistentStateRef>("stateRef")))
|
||||
criteriaQuery.where(joinPredicate1, joinPredicate2)
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults).hasSize(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* DummyLinearSchemaV2 = optimised DummyLinear schema (extending LinearState)
|
||||
*/
|
||||
|
||||
@Test
|
||||
fun `three way join by composite primary between VaultStates, VaultLinearStates and DummyLinearStates`() {
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(8)
|
||||
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
|
||||
services.fillWithSomeTestLinearStates(2)
|
||||
}
|
||||
|
||||
val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2)
|
||||
val criteriaBuilder = sessionFactory.criteriaBuilder
|
||||
val entityManager = sessionFactory.createEntityManager()
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java)
|
||||
val dummyLinearStates = criteriaQuery.from(DummyLinearStateSchemaV2.PersistentDummyLinearState::class.java)
|
||||
|
||||
criteriaQuery.select(vaultStates)
|
||||
val joinPredicate1 = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultLinearStates.get<PersistentStateRef>("stateRef"))
|
||||
val joinPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), dummyLinearStates.get<PersistentStateRef>("stateRef")))
|
||||
criteriaQuery.where(joinPredicate1, joinPredicate2)
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults).hasSize(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a OneToOne table mapping
|
||||
*/
|
||||
@Test
|
||||
fun `select fungible states by owner party`() {
|
||||
database.transaction {
|
||||
// persist original cash states explicitly with V3 schema
|
||||
cashStates.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3)
|
||||
}
|
||||
}
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(SampleCashSchemaV3.PersistentCashState::class.java)
|
||||
criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java)
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults).hasSize(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Query by Party (OneToOne table mapping)
|
||||
*/
|
||||
@Test
|
||||
fun `query fungible states by owner party`() {
|
||||
database.transaction {
|
||||
// persist original cash states explicitly with V3 schema
|
||||
cashStates.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3)
|
||||
}
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = ALICE)
|
||||
val cashStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L),
|
||||
issuedBy = BOB.ref(0), issuerKey = BOB_KEY, ownedBy = (BOB)).states
|
||||
// persist additional cash states explicitly with V3 schema
|
||||
cashStates.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3)
|
||||
}
|
||||
}
|
||||
|
||||
val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, CommonSchemaV1, SampleCashSchemaV3)
|
||||
val criteriaBuilder = sessionFactory.criteriaBuilder
|
||||
val entityManager = sessionFactory.createEntityManager()
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
|
||||
// select
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
criteriaQuery.select(vaultStates)
|
||||
|
||||
// search predicate
|
||||
val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java)
|
||||
|
||||
val joinCashToParty = cashStatesSchema.join<SampleCashSchemaV3.PersistentCashState,CommonSchemaV1.Party>("owner")
|
||||
val queryOwnerKey = BOB_PUBKEY.toBase58String()
|
||||
criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get<CommonSchemaV1.Party>("key"), queryOwnerKey))
|
||||
|
||||
val joinVaultStatesToCash = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), cashStatesSchema.get<PersistentStateRef>("stateRef"))
|
||||
criteriaQuery.where(joinVaultStatesToCash)
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
|
||||
queryResults.forEach {
|
||||
val contractState = it.contractState.deserialize<TransactionState<ContractState>>(storageKryo())
|
||||
val cashState = contractState.data as Cash.State
|
||||
println("${it.stateRef} with owner: ${cashState.owner.owningKey.toBase58String()}") }
|
||||
|
||||
assertThat(queryResults).hasSize(12)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a OneToMany table mapping
|
||||
*/
|
||||
@Test
|
||||
fun `select fungible states by participants`() {
|
||||
database.transaction {
|
||||
// persist cash states explicitly with V2 schema
|
||||
cashStates.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3)
|
||||
}
|
||||
}
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(SampleCashSchemaV3.PersistentCashState::class.java)
|
||||
criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java)
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
|
||||
assertThat(queryResults).hasSize(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Query by participants (OneToMany table mapping)
|
||||
*/
|
||||
@Test
|
||||
fun `query fungible states by participants`() {
|
||||
val firstCashState =
|
||||
database.transaction {
|
||||
// persist original cash states explicitly with V3 schema
|
||||
cashStates.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3)
|
||||
}
|
||||
|
||||
val moreCash = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L),
|
||||
issuedBy = BOB.ref(0), issuerKey = BOB_KEY, ownedBy = BOB).states
|
||||
// persist additional cash states explicitly with V3 schema
|
||||
moreCash.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3)
|
||||
}
|
||||
|
||||
val cashStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), ownedBy = (ALICE)).states
|
||||
// persist additional cash states explicitly with V3 schema
|
||||
cashStates.forEach {
|
||||
val cashState = it.state.data
|
||||
val dummyFungibleState = DummyFungibleContract.State(cashState.amount, cashState.owner)
|
||||
hibernatePersister.persistStateWithSchema(dummyFungibleState, it.ref, SampleCashSchemaV3)
|
||||
}
|
||||
cashStates.first()
|
||||
}
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
|
||||
// select
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
criteriaQuery.select(vaultStates)
|
||||
|
||||
// search predicate
|
||||
val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java)
|
||||
|
||||
val joinCashToParty = cashStatesSchema.join<SampleCashSchemaV3.PersistentCashState, CommonSchemaV1.Party>("participants")
|
||||
val queryParticipantKeys = firstCashState.state.data.participants.map { it.owningKey.toBase58String() }
|
||||
criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get<CommonSchemaV1.Party>("key"), queryParticipantKeys))
|
||||
|
||||
val joinVaultStatesToCash = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), cashStatesSchema.get<PersistentStateRef>("stateRef"))
|
||||
criteriaQuery.where(joinVaultStatesToCash)
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
queryResults.forEach {
|
||||
val contractState = it.contractState.deserialize<TransactionState<ContractState>>(storageKryo())
|
||||
val cashState = contractState.data as Cash.State
|
||||
println("${it.stateRef} with owner ${cashState.owner.owningKey.toBase58String()} and participants ${cashState.participants.map { it.owningKey.toBase58String() }}")
|
||||
}
|
||||
|
||||
assertThat(queryResults).hasSize(12)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query with sorting on Common table attribute
|
||||
*/
|
||||
@Test
|
||||
fun `with sorting on attribute from common table`() {
|
||||
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(1, externalId = "111")
|
||||
services.fillWithSomeTestLinearStates(2, externalId = "222")
|
||||
services.fillWithSomeTestLinearStates(3, externalId = "333")
|
||||
}
|
||||
|
||||
val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV2)
|
||||
val criteriaBuilder = sessionFactory.criteriaBuilder
|
||||
val entityManager = sessionFactory.createEntityManager()
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java)
|
||||
|
||||
// join
|
||||
criteriaQuery.multiselect(vaultStates, vaultLinearStates)
|
||||
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultLinearStates.get<PersistentStateRef>("stateRef"))
|
||||
criteriaQuery.where(joinPredicate)
|
||||
|
||||
// order by DESC
|
||||
criteriaQuery.orderBy(criteriaBuilder.desc(vaultLinearStates.get<String>("externalId")))
|
||||
criteriaQuery.orderBy(criteriaBuilder.desc(vaultLinearStates.get<UUID>("uuid")))
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
queryResults.map {
|
||||
val vaultState = it[0] as VaultSchemaV1.VaultStates
|
||||
val vaultLinearState = it[1] as VaultSchemaV1.VaultLinearStates
|
||||
println("${vaultState.stateRef} : ${vaultLinearState.externalId} ${vaultLinearState.uuid}")
|
||||
}
|
||||
|
||||
// order by ASC
|
||||
criteriaQuery.orderBy(criteriaBuilder.asc(vaultLinearStates.get<String>("externalId")))
|
||||
criteriaQuery.orderBy(criteriaBuilder.asc(vaultLinearStates.get<UUID>("uuid")))
|
||||
|
||||
// execute query
|
||||
val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList
|
||||
queryResultsAsc.map {
|
||||
val vaultState = it[0] as VaultSchemaV1.VaultStates
|
||||
val vaultLinearState = it[1] as VaultSchemaV1.VaultLinearStates
|
||||
println("${vaultState.stateRef} : ${vaultLinearState.externalId} ${vaultLinearState.uuid}")
|
||||
}
|
||||
|
||||
assertThat(queryResults).hasSize(6)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query with sorting on Custom table attribute
|
||||
*/
|
||||
@Test
|
||||
fun `with sorting on attribute from custom table`() {
|
||||
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(1, externalId = "111")
|
||||
services.fillWithSomeTestLinearStates(2, externalId = "222")
|
||||
services.fillWithSomeTestLinearStates(3, externalId = "333")
|
||||
}
|
||||
|
||||
val sessionFactory = hibernateConfig.sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1)
|
||||
val criteriaBuilder = sessionFactory.criteriaBuilder
|
||||
val entityManager = sessionFactory.createEntityManager()
|
||||
|
||||
// structure query
|
||||
val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java)
|
||||
val dummyLinearStates = criteriaQuery.from(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java)
|
||||
|
||||
// join
|
||||
criteriaQuery.multiselect(vaultStates, vaultLinearStates, dummyLinearStates)
|
||||
val joinPredicate1 = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultLinearStates.get<PersistentStateRef>("stateRef"))
|
||||
val joinPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), dummyLinearStates.get<PersistentStateRef>("stateRef")))
|
||||
criteriaQuery.where(joinPredicate1, joinPredicate2)
|
||||
|
||||
// order by DESC
|
||||
criteriaQuery.orderBy(criteriaBuilder.desc(dummyLinearStates.get<String>("externalId")))
|
||||
criteriaQuery.orderBy(criteriaBuilder.desc(dummyLinearStates.get<UUID>("uuid")))
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
queryResults.map {
|
||||
val vaultState = it[0] as VaultSchemaV1.VaultStates
|
||||
val _vaultLinearStates = it[1] as VaultSchemaV1.VaultLinearStates
|
||||
val _dummyLinearStates = it[2] as DummyLinearStateSchemaV1.PersistentDummyLinearState
|
||||
println("${vaultState.stateRef} : [${_dummyLinearStates.externalId} ${_dummyLinearStates.uuid}] : [${_vaultLinearStates.externalId} ${_vaultLinearStates.uuid}]")
|
||||
}
|
||||
|
||||
// order by ASC
|
||||
criteriaQuery.orderBy(criteriaBuilder.asc(dummyLinearStates.get<String>("externalId")))
|
||||
criteriaQuery.orderBy(criteriaBuilder.asc(dummyLinearStates.get<UUID>("uuid")))
|
||||
|
||||
// execute query
|
||||
val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList
|
||||
queryResultsAsc.map {
|
||||
val vaultState = it[0] as VaultSchemaV1.VaultStates
|
||||
val _vaultLinearStates = it[1] as VaultSchemaV1.VaultLinearStates
|
||||
val _dummyLinearStates = it[2] as DummyLinearStateSchemaV1.PersistentDummyLinearState
|
||||
println("${vaultState.stateRef} : [${_dummyLinearStates.externalId} ${_dummyLinearStates.uuid}] : [${_vaultLinearStates.externalId} ${_vaultLinearStates.uuid}]")
|
||||
}
|
||||
|
||||
assertThat(queryResults).hasSize(6)
|
||||
}
|
||||
|
||||
}
|
@ -19,10 +19,10 @@ import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_PUBKEY_1
|
||||
import net.corda.node.services.persistence.DBTransactionStorage
|
||||
import net.corda.node.services.vault.schemas.Models
|
||||
import net.corda.node.services.vault.schemas.VaultCashBalancesEntity
|
||||
import net.corda.node.services.vault.schemas.VaultSchema
|
||||
import net.corda.node.services.vault.schemas.VaultStatesEntity
|
||||
import net.corda.node.services.vault.schemas.requery.Models
|
||||
import net.corda.node.services.vault.schemas.requery.VaultCashBalancesEntity
|
||||
import net.corda.node.services.vault.schemas.requery.VaultSchema
|
||||
import net.corda.node.services.vault.schemas.requery.VaultStatesEntity
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.node.utilities.transaction
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
@ -30,6 +30,7 @@ import org.assertj.core.api.Assertions
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.Closeable
|
||||
@ -123,6 +124,55 @@ class RequeryConfigurationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bounded iteration`() {
|
||||
// insert 100 entities
|
||||
database.transaction {
|
||||
requerySession.withTransaction {
|
||||
(1..100)
|
||||
.map { newTransaction(it) }
|
||||
.forEach { insert(createVaultStateEntity(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
// query entities 41..45
|
||||
database.transaction {
|
||||
requerySession.withTransaction {
|
||||
// Note: cannot specify a limit explicitly when using iterator skip & take
|
||||
val query = select(VaultSchema.VaultStates::class)
|
||||
val count = query.get().count()
|
||||
Assertions.assertThat(count).isEqualTo(100)
|
||||
val result = query.get().iterator(40, 5)
|
||||
Assertions.assertThat(result.asSequence().count()).isEqualTo(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test calling an arbitrary JDBC native query`() {
|
||||
val txn = newTransaction()
|
||||
|
||||
database.transaction {
|
||||
transactionStorage.addTransaction(txn)
|
||||
requerySession.withTransaction {
|
||||
insert(createVaultStateEntity(txn))
|
||||
}
|
||||
}
|
||||
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
val nativeQuery = "SELECT v.transaction_id, v.output_index FROM vault_states v WHERE v.state_status = 0"
|
||||
|
||||
database.transaction {
|
||||
val configuration = RequeryConfiguration(dataSourceProperties, true)
|
||||
val jdbcSession = configuration.jdbcSession()
|
||||
val prepStatement = jdbcSession.prepareStatement(nativeQuery)
|
||||
val rs = prepStatement.executeQuery()
|
||||
assertTrue(rs.next())
|
||||
assertEquals(rs.getString(1), txn.tx.inputs[0].txhash.toString())
|
||||
assertEquals(rs.getInt(2), txn.tx.inputs[0].index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createVaultStateEntity(txn: SignedTransaction): VaultStatesEntity {
|
||||
val txnState = txn.tx.inputs[0]
|
||||
val state = VaultStatesEntity().apply {
|
||||
@ -158,9 +208,9 @@ class RequeryConfigurationTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun newTransaction(): SignedTransaction {
|
||||
private fun newTransaction(index: Int = 0): SignedTransaction {
|
||||
val wtx = WireTransaction(
|
||||
inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)),
|
||||
inputs = listOf(StateRef(SecureHash.randomSHA256(), index)),
|
||||
attachments = emptyList(),
|
||||
outputs = emptyList(),
|
||||
commands = emptyList(),
|
||||
|
@ -11,7 +11,7 @@ import net.corda.core.utilities.LogHelper
|
||||
import net.corda.core.write
|
||||
import net.corda.core.writeLines
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.persistence.schemas.AttachmentEntity
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.node.utilities.transaction
|
||||
|
@ -1,9 +1,6 @@
|
||||
package net.corda.node.services.schema
|
||||
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -13,6 +10,7 @@ import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.utilities.LogHelper
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.schema.HibernateObserver
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.node.utilities.transaction
|
||||
@ -99,9 +97,9 @@ class HibernateObserverTests {
|
||||
val schemaService = object : SchemaService {
|
||||
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = emptyMap()
|
||||
|
||||
override fun selectSchemas(state: QueryableState): Iterable<MappedSchema> = setOf(testSchema)
|
||||
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> = setOf(testSchema)
|
||||
|
||||
override fun generateMappedObject(state: QueryableState, schema: MappedSchema): PersistentState {
|
||||
override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState {
|
||||
val parent = Parent()
|
||||
parent.children.add(Child())
|
||||
parent.children.add(Child())
|
||||
@ -110,14 +108,14 @@ class HibernateObserverTests {
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val observer = HibernateObserver(rawUpdatesPublisher, schemaService)
|
||||
val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService))
|
||||
database.transaction {
|
||||
rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0)))))
|
||||
val parentRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from contract_Parents").executeQuery()
|
||||
val parentRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery()
|
||||
parentRowCountResult.next()
|
||||
val parentRows = parentRowCountResult.getInt(1)
|
||||
parentRowCountResult.close()
|
||||
val childrenRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from contract_Children").executeQuery()
|
||||
val childrenRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery()
|
||||
childrenRowCountResult.next()
|
||||
val childrenRows = childrenRowCountResult.getInt(1)
|
||||
childrenRowCountResult.close()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import net.corda.contracts.DummyDealContract
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.contracts.testing.*
|
||||
|
@ -13,6 +13,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.DUMMY_CA
|
||||
import net.corda.core.utilities.getTestPartyAndCertificate
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.services.keys.freshCertificate
|
||||
import net.corda.node.services.keys.getSigner
|
||||
@ -69,15 +70,15 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub {
|
||||
override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *keys)
|
||||
|
||||
override val vaultService: VaultService get() = throw UnsupportedOperationException()
|
||||
override val vaultQueryService: VaultQueryService get() = throw UnsupportedOperationException()
|
||||
override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException()
|
||||
override val clock: Clock get() = Clock.systemUTC()
|
||||
override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, getTestPartyAndCertificate(MEGA_CORP.name, key.public), MOCK_VERSION_INFO.platformVersion)
|
||||
override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2)
|
||||
|
||||
fun makeVaultService(dataSourceProps: Properties): VaultService {
|
||||
fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService())): VaultService {
|
||||
val vaultService = NodeVaultService(this, dataSourceProps)
|
||||
// Vault cash spending requires access to contract_cash_states and their updates
|
||||
HibernateObserver(vaultService.rawUpdates, NodeSchemaService())
|
||||
HibernateObserver(vaultService.rawUpdates, hibernateConfig)
|
||||
return vaultService
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user