Improved support for testing custom schemas using a MockNetwork. (#1450)

* Improved support for testing custom schemas using a MockNetwork.

* Removed `requiredSchemas` from CordaPluginREgistry configuration
Custom schema registration now uses classpath scanning (CorDapps) and explicit registration (tests) following same mechanisms as flow registration.

* Updated following PR review feedback.

* Helper function to return Kotlin object instance fixed and moved to core InternalUtils class.

* Fixed auto-scanning Unit test to assert correct registration of custom schema.

* cleanup comment.

* Changes following rebase from master.
This commit is contained in:
josecoll
2017-09-13 12:06:24 +01:00
committed by GitHub
parent 93101f7c7d
commit 5504493c8d
28 changed files with 203 additions and 101 deletions

View File

@ -24,6 +24,7 @@ import net.corda.core.node.PluginServiceHub
import net.corda.core.node.ServiceEntry
import net.corda.core.node.services.*
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction
@ -73,7 +74,6 @@ import rx.Observable
import java.io.IOException
import java.lang.reflect.InvocationTargetException
import java.nio.file.Path
import java.nio.file.Paths
import java.security.KeyPair
import java.security.KeyStoreException
import java.security.cert.CertificateFactory
@ -214,6 +214,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
installCordaServices()
registerCordappFlows()
_services.rpcFlows += cordappLoader.findRPCFlows()
registerCustomSchemas(cordappLoader.findCustomSchemas())
runOnStop += network::stop
}
@ -655,7 +656,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
override val monitoringService = MonitoringService(MetricRegistry())
override val validatedTransactions = makeTransactionStorage()
override val transactionVerifierService by lazy { makeTransactionVerifierService() }
override val schemaService by lazy { NodeSchemaService(pluginRegistries.flatMap { it.requiredSchemas }.toSet()) }
override val schemaService by lazy { NodeSchemaService() }
override val networkMapCache by lazy { PersistentNetworkMapCache(this) }
override val vaultService by lazy { NodeVaultService(this) }
override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() }
@ -703,4 +704,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
override fun jdbcSession(): Connection = database.createSession()
}
fun registerCustomSchemas(schemas: Set<MappedSchema>) {
database.hibernateConfig.schemaService.registerCustomSchemas(schemas)
}
}

View File

@ -6,13 +6,11 @@ import net.corda.core.flows.ContractUpgradeFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.internal.isRegularFile
import net.corda.core.internal.list
import net.corda.core.internal.*
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.CordaService
import net.corda.core.node.services.ServiceType
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
@ -121,6 +119,10 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
return found + coreFlows
}
fun findCustomSchemas(): Set<MappedSchema> {
return scanResult?.getClassesWithSuperclass(MappedSchema::class)?.toSet() ?: emptySet()
}
private fun scanCordapps(): ScanResult? {
logger.info("Scanning CorDapps in $cordappClassPath")
return if (cordappClassPath.isNotEmpty())
@ -144,21 +146,28 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
}
}
private fun <T : Any> ScanResult.getClassesWithAnnotation(type: KClass<T>, annotation: KClass<out Annotation>): List<Class<out T>> {
fun loadClass(className: String): Class<out T>? {
return try {
appClassLoader.loadClass(className) as Class<T>
} catch (e: ClassCastException) {
logger.warn("As $className is annotated with ${annotation.qualifiedName} it must be a sub-type of ${type.java.name}")
null
} catch (e: Exception) {
logger.warn("Unable to load class $className", e)
null
}
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
return try {
appClassLoader.loadClass(className) as Class<T>
} catch (e: ClassCastException) {
logger.warn("As $className must be a sub-type of ${type.java.name}")
null
} catch (e: Exception) {
logger.warn("Unable to load class $className", e)
null
}
}
private fun <T : Any> ScanResult.getClassesWithSuperclass(type: KClass<T>): List<T> {
return getNamesOfSubclassesOf(type.java)
.mapNotNull { loadClass(it, type) }
.filterNot { Modifier.isAbstract(it.modifiers) }
.map { it.kotlin.objectOrNewInstance() }
}
private fun <T : Any> ScanResult.getClassesWithAnnotation(type: KClass<T>, annotation: KClass<out Annotation>): List<Class<out T>> {
return getNamesOfClassesWithAnnotation(annotation.java)
.mapNotNull { loadClass(it) }
.mapNotNull { loadClass(it, type) }
.filterNot { Modifier.isAbstract(it.modifiers) }
}
}

View File

@ -30,5 +30,11 @@ interface SchemaService {
* or via custom logic in this service.
*/
fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState
/**
* Registration mechanism to add custom contract schemas that extend the [MappedSchema] class.
*/
fun registerCustomSchemas(customSchemas: Set<MappedSchema>)
}
//DOCEND SchemaService

View File

@ -61,13 +61,13 @@ class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaS
// 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> =
private val requiredSchemas: Map<MappedSchema, SchemaService.SchemaOptions> =
mapOf(Pair(CommonSchemaV1, SchemaService.SchemaOptions()),
Pair(VaultSchemaV1, SchemaService.SchemaOptions()),
Pair(NodeInfoSchemaV1, SchemaService.SchemaOptions()),
Pair(NodeServicesV1, SchemaService.SchemaOptions()))
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas.plus(customSchemas.map {
override var schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas.plus(customSchemas.map {
mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions())
})
@ -92,4 +92,10 @@ class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaS
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)
}
override fun registerCustomSchemas(_customSchemas: Set<MappedSchema>) {
schemaOptions = schemaOptions.plus(_customSchemas.map {
mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions())
})
}
}

View File

@ -321,9 +321,8 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
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""")
Please register the entity '${entityClass.name.substringBefore('$')}'
See https://docs.corda.net/api-persistence.html#custom-schema-registration for more information""")
}
throw VaultQueryException("Parsing error: ${e.message}")
}

View File

@ -28,14 +28,14 @@ import java.lang.Exception
import java.util.*
import javax.persistence.Tuple
class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration,
class HibernateVaultQueryImpl(val hibernateConfig: HibernateConfiguration,
val vault: VaultService) : SingletonSerializeAsToken(), VaultQueryService {
companion object {
val log = loggerFor<HibernateVaultQueryImpl>()
}
private val sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas()
private val criteriaBuilder = sessionFactory.criteriaBuilder
private var sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas()
private var criteriaBuilder = sessionFactory.criteriaBuilder
/**
* Maintain a list of contract state interfaces to concrete types stored in the vault
@ -64,6 +64,10 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration,
override fun <T : ContractState> _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class<out T>): Vault.Page<T> {
log.info("Vault Query for contract type: $contractType, criteria: $criteria, pagination: $paging, sorting: $sorting")
// refresh to include any schemas registered after initial VQ service initialisation
sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas()
criteriaBuilder = sessionFactory.criteriaBuilder
// calculate total results where a page specification has been defined
var totalStates = -1L
if (!paging.isDefault) {

View File

@ -65,8 +65,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
ArrayList<KeyPair> keys = new ArrayList<>();
keys.add(getMEGA_CORP_KEY());
keys.add(getDUMMY_NOTARY_KEY());
Set<MappedSchema> requiredSchemas = new HashSet<>();
requiredSchemas.add(CashSchemaV1.INSTANCE);
Set<MappedSchema> requiredSchemas = Collections.singleton(CashSchemaV1.INSTANCE);
IdentityService identitySvc = makeTestIdentityService();
@SuppressWarnings("unchecked")
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc);

View File

@ -43,10 +43,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() {
LogHelper.setLevel(PersistentUniquenessProvider::class)
val dataSourceProps = makeTestDataSourceProperties()
val transactionSchema = MappedSchema(schemaFamily = javaClass, version = 1,
mappedTypes = listOf(DBTransactionStorage.DBTransaction::class.java))
val createSchemaService = { NodeSchemaService(setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3, transactionSchema)) }
val createSchemaService = { NodeSchemaService() }
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), createSchemaService, ::makeTestIdentityService)

View File

@ -76,8 +76,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY)
val dataSourceProps = makeTestDataSourceProperties()
val defaultDatabaseProperties = makeTestDatabaseProperties()
val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3)
val createSchemaService = { NodeSchemaService(customSchemas) }
val createSchemaService = { NodeSchemaService() }
database = configureDatabase(dataSourceProps, defaultDatabaseProperties, createSchemaService, ::makeTestIdentityService)
database.transaction {
hibernateConfig = database.hibernateConfig
@ -97,6 +96,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
}
setUpDb()
val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3)
sessionFactory = hibernateConfig.sessionFactoryForSchemas(*customSchemas.toTypedArray())
entityManager = sessionFactory.createEntityManager()
criteriaBuilder = sessionFactory.criteriaBuilder

View File

@ -7,22 +7,19 @@ import net.corda.core.node.services.Vault
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState
import net.corda.testing.LogHelper
import net.corda.node.services.api.SchemaService
import net.corda.node.utilities.DatabaseTransactionManager
import net.corda.node.utilities.configureDatabase
import net.corda.testing.LogHelper
import net.corda.testing.MEGA_CORP
import net.corda.testing.contracts.DUMMY_PROGRAM_ID
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties
import net.corda.testing.node.MockServices.Companion.makeTestIdentityService
import org.hibernate.annotations.Cascade
import org.hibernate.annotations.CascadeType
import org.junit.After
import org.junit.Before
import org.junit.Test
import rx.subjects.PublishSubject
import javax.persistence.*
import kotlin.test.assertEquals
@ -38,32 +35,6 @@ class HibernateObserverTests {
LogHelper.reset(HibernateObserver::class)
}
class SchemaFamily
@Entity
@Table(name = "Parents")
class Parent : PersistentState() {
@OneToMany(fetch = FetchType.LAZY)
@JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index"))
@OrderColumn
@Cascade(CascadeType.PERSIST)
var children: MutableSet<Child> = mutableSetOf()
}
@Suppress("unused")
@Entity
@Table(name = "Children")
class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "child_id", unique = true, nullable = false)
var childId: Int? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index"))
var parent: Parent? = null
}
class TestState : QueryableState {
override fun supportedSchemas(): Iterable<MappedSchema> {
throw UnsupportedOperationException()
@ -80,17 +51,19 @@ class HibernateObserverTests {
// This method does not use back quotes for a nice name since it seems to kill the kotlin compiler.
@Test
fun testChildObjectsArePersisted() {
val testSchema = object : MappedSchema(SchemaFamily::class.java, 1, setOf(Parent::class.java, Child::class.java)) {}
val testSchema = TestSchema
val rawUpdatesPublisher = PublishSubject.create<Vault.Update<ContractState>>()
val schemaService = object : SchemaService {
override fun registerCustomSchemas(customSchemas: Set<MappedSchema>) {}
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = emptyMap()
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> = setOf(testSchema)
override fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState {
val parent = Parent()
parent.children.add(Child())
parent.children.add(Child())
val parent = TestSchema.Parent()
parent.children.add(TestSchema.Child())
parent.children.add(TestSchema.Child())
return parent
}
}

View File

@ -0,0 +1,89 @@
package net.corda.node.services.schema
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.messaging.startFlow
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.api.ServiceHubInternal
import net.corda.testing.driver.driver
import net.corda.testing.node.MockNetwork
import net.corda.testing.schemas.DummyLinearStateSchemaV1
import org.hibernate.annotations.Cascade
import org.hibernate.annotations.CascadeType
import org.junit.Test
import javax.persistence.*
import kotlin.test.assertTrue
class NodeSchemaServiceTest {
/**
* Note: this test requires explicitly registering custom contract schemas with a MockNode
*/
@Test
fun `registering custom schemas for testing with MockNode`() {
val mockNet = MockNetwork()
val mockNode = mockNet.createNode()
mockNet.runNetwork()
mockNode.registerCustomSchemas(setOf(DummyLinearStateSchemaV1))
val schemaService = mockNode.services.schemaService
assertTrue(schemaService.schemaOptions.containsKey(DummyLinearStateSchemaV1))
mockNet.stopNodes()
}
/**
* Note: this test verifies auto-scanning to register identified [MappedSchema] schemas.
* By default, Driver uses the caller package for auto-scanning:
* System.setProperty("net.corda.node.cordapp.scan.package", callerPackage)
*/
@Test
fun `auto scanning of custom schemas for testing with Driver`() {
driver (startNodesInProcess = true) {
val node = startNode()
val nodeHandle = node.getOrThrow()
val result = nodeHandle.rpc.startFlow(::MappedSchemasFlow)
val mappedSchemas = result.returnValue.getOrThrow()
assertTrue(mappedSchemas.contains(TestSchema.name))
}
}
@StartableByRPC
class MappedSchemasFlow : FlowLogic<List<String>>() {
@Suspendable
override fun call() : List<String> {
// returning MappedSchema's as String'ified family names to avoid whitelist serialization errors
return (this.serviceHub as ServiceHubInternal).schemaService.schemaOptions.keys.map { it.name }
}
}
}
class SchemaFamily
object TestSchema : MappedSchema(SchemaFamily::class.java, 1, setOf(Parent::class.java, Child::class.java)) {
@Entity
@Table(name = "Parents")
class Parent : PersistentState() {
@OneToMany(fetch = FetchType.LAZY)
@JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index"))
@OrderColumn
@Cascade(CascadeType.PERSIST)
var children: MutableSet<Child> = mutableSetOf()
}
@Suppress("unused")
@Entity
@Table(name = "Children")
class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "child_id", unique = true, nullable = false)
var childId: Int? = null
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index"))
var parent: Parent? = null
}
}

View File

@ -23,6 +23,7 @@ import net.corda.finance.contracts.asset.Cash
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY
import net.corda.finance.contracts.getCashBalance
import net.corda.finance.schemas.CashSchemaV1
import net.corda.finance.utils.sumCash
import net.corda.node.utilities.CordaPersistence
import net.corda.testing.*
@ -53,7 +54,8 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Before
fun setUp() {
LogHelper.setLevel(NodeVaultService::class)
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BOC_KEY, DUMMY_CASH_ISSUER_KEY))
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BOC_KEY, DUMMY_CASH_ISSUER_KEY),
customSchemas = setOf(CashSchemaV1))
database = databaseAndServices.first
services = databaseAndServices.second
issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOC_KEY)

View File

@ -63,7 +63,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
// register additional identities
identitySvc.verifyAndRegisterIdentity(CASH_NOTARY_IDENTITY)
identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY)
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), createIdentityService = { identitySvc })
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY),
createIdentityService = { identitySvc },
customSchemas = setOf(CashSchemaV1, CommercialPaperSchemaV1, DummyLinearStateSchemaV1))
database = databaseAndServices.first
services = databaseAndServices.second
notaryServices = MockServices(DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY)

View File

@ -16,6 +16,7 @@ import net.corda.finance.contracts.asset.Cash
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY
import net.corda.finance.contracts.getCashBalance
import net.corda.finance.schemas.CashSchemaV1
import net.corda.node.utilities.CordaPersistence
import net.corda.testing.*
import net.corda.testing.contracts.*
@ -44,7 +45,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
@Before
fun setUp() {
LogHelper.setLevel(VaultWithCashTest::class)
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(DUMMY_CASH_ISSUER_KEY, DUMMY_NOTARY_KEY))
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(DUMMY_CASH_ISSUER_KEY, DUMMY_NOTARY_KEY),
customSchemas = setOf(CashSchemaV1))
database = databaseAndServices.first
services = databaseAndServices.second
issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY)