diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 70724cb884..b5b8bc6f72 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -242,10 +242,14 @@ private fun IntProgression.toSpliterator(): Spliterator.OfInt { } fun IntProgression.stream(parallel: Boolean = false): IntStream = StreamSupport.intStream(toSpliterator(), parallel) - +inline fun Stream.toTypedArray() = toTypedArray(T::class.java) // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable): -inline fun Stream.toTypedArray(): Array = uncheckedCast(toArray { size -> arrayOfNulls(size) }) +fun Stream.toTypedArray(componentType: Class): Array = toArray { size -> + uncheckedCast>(java.lang.reflect.Array.newInstance(componentType, size)) +} +fun Stream.filterNotNull(): Stream = uncheckedCast(filter(Objects::nonNull)) +fun Stream>.toMap(): Map = collect>(::LinkedHashMap, { m, (k, v) -> m.put(k, v) }, { m, t -> m.putAll(t) }) fun Class.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) else null /** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [Class]. */ diff --git a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt index 0a2fb69f26..12818f9a81 100644 --- a/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/InternalUtilsTest.kt @@ -3,6 +3,7 @@ package net.corda.core.internal import org.assertj.core.api.Assertions import org.junit.Assert.assertArrayEquals import org.junit.Test +import java.io.Serializable import java.util.stream.IntStream import java.util.stream.Stream import kotlin.test.assertEquals @@ -87,5 +88,17 @@ class InternalUtilsTest { val b: Array = Stream.of("one", "two", null).toTypedArray() assertEquals(Array::class.java, b.javaClass) assertArrayEquals(arrayOf("one", "two", null), b) + val c: Array = Stream.of("x", "y").toTypedArray(CharSequence::class.java) + assertEquals(Array::class.java, c.javaClass) + assertArrayEquals(arrayOf("x", "y"), c) + val d: Array = Stream.of("x", "y", null).toTypedArray(uncheckedCast(CharSequence::class.java)) + assertEquals(Array::class.java, d.javaClass) + assertArrayEquals(arrayOf("x", "y", null), d) + } + + @Test + fun `Stream of Pairs toMap works`() { + val m: Map, Serializable> = Stream.of, Serializable>>("x" to "y", 0 to 1, "x" to '2').toMap() + assertEquals>(mapOf("x" to '2', 0 to 1), m) } } diff --git a/node/src/main/kotlin/net/corda/lazyhub/LazyHub.kt b/node/src/main/kotlin/net/corda/lazyhub/LazyHub.kt new file mode 100644 index 0000000000..512fa1c74e --- /dev/null +++ b/node/src/main/kotlin/net/corda/lazyhub/LazyHub.kt @@ -0,0 +1,109 @@ +package net.corda.lazyhub + +import net.corda.core.serialization.CordaSerializable +import kotlin.reflect.KClass +import kotlin.reflect.KFunction + +/** Supertype of all exceptions thrown directly by [LazyHub]. */ +@CordaSerializable +abstract class LazyHubException(message: String) : RuntimeException(message) + +/** The type can't be instantiated because it is abstract, i.e. it's an interface or abstract class. */ +class AbstractTypeException(message: String) : LazyHubException(message) + +/** + * The class can't be instantiated because it has no public constructor. + * This is so that you can easily hide a constructor from LazyHub by making it non-public. + */ +class NoPublicConstructorsException(message: String) : LazyHubException(message) + +/** + * Nullable factory return types are not supported, as LazyHub has no concept of a provider that MAY supply an object. + * If you want an optional result, use logic to decide whether to add the factory to the lazyHub. + */ +class NullableReturnTypeException(message: String) : LazyHubException(message) + +/** The parameter can't be satisfied and doesn't have a default and isn't nullable. */ +abstract class UnsatisfiableParamException(message: String) : LazyHubException(message) + +/** No provider has been registered for the wanted type. */ +class NoSuchProviderException(message: String) : UnsatisfiableParamException(message) + +/** + * No provider has been registered for the component type of the wanted array. + * Note that LazyHub does not create empty arrays, make the array param type nullable to accept no elements. + * This allows you to express zero-or-more (nullable) or one-or-more via the parameter type. + */ +class UnsatisfiableArrayException(message: String) : UnsatisfiableParamException(message) + +/** More than one provider has been registered for the type but at most one object is wanted. */ +class TooManyProvidersException(message: String) : UnsatisfiableParamException(message) + +/** + * More than one public constructor is satisfiable and there is no clear winner. + * The winner is the constructor with the most params for which LazyHub actually supplies an arg. + */ +class NoUniqueGreediestSatisfiableConstructorException(message: String) : LazyHubException(message) + +/** The object being created depends on itself, i.e. it's already being instantiated/factoried. */ +class CircularDependencyException(message: String) : LazyHubException(message) + +/** Depend on this as a param (and add the [MutableLazyHub], which is a [LazyHubFactory], to itself) if you want to make child containers. */ +interface LazyHubFactory { + fun child(): MutableLazyHub +} + +/** + * Read-only interface to the lazyHub. + * Where possible, always obtain your object via a constructor/method param instead of directly from the [LazyHub]. + * This results in the greatest automatic benefits to the codebase e.g. separation of concerns and ease of testing. + * A notable exception to this rule is `getAll(Unit::class)` to (idempotently) run all side-effects. + */ +interface LazyHub : LazyHubFactory { + operator fun get(clazz: KClass) = get(clazz.java) + operator fun get(clazz: Class) = getOrNull(clazz) ?: throw NoSuchProviderException(clazz.toString()) + fun getAll(clazz: KClass) = getAll(clazz.java) + fun getAll(clazz: Class): List + fun getOrNull(clazz: KClass) = getOrNull(clazz.java) + fun getOrNull(clazz: Class): T? +} + +/** Fully-featured interface to the lazyHub. */ +interface MutableLazyHub : LazyHub { + /** Register the given object against its class and all supertypes. */ + fun obj(obj: Any) + + /** Like plain old [MutableLazyHub.obj] but removes all [service] providers first. */ + fun obj(service: KClass, obj: T) + + /** + * Register the given class as a provider for itself and all supertypes. + * The class is instantiated at most once, using the greediest public constructor satisfiable at the time. + */ + fun impl(impl: KClass<*>) + + /** + * Same as [MutableLazyHub.impl] if you don't have a static reference to the class. + * Note that Kotlin features such as nullable params and default args will not be available. + */ + fun impl(impl: Class<*>) + + /** Like plain old [MutableLazyHub.impl] but removes all [service] providers first. */ + fun impl(service: KClass, impl: KClass) + + /** Like the [KClass] variant if you don't have a static reference fo the class. */ + fun impl(service: KClass, impl: Class) + + /** + * Register the given function as a provider for its **declared** return type and all supertypes. + * The function is invoked at most once. Unlike constructors, the function may have any visibility. + * By convention the function should have side-effects iff its return type is [Unit]. + */ + fun factory(factory: KFunction<*>) + + /** Register a factory that provides the given type from the given hub. */ + fun factory(lh: LazyHub, type: KClass<*>) + + /** Like plain old [MutableLazyHub.factory] but removes all [service] providers first. */ + fun factory(service: KClass, factory: KFunction) +} diff --git a/node/src/main/kotlin/net/corda/lazyhub/LazyHubImpl.kt b/node/src/main/kotlin/net/corda/lazyhub/LazyHubImpl.kt new file mode 100644 index 0000000000..5edbb4027b --- /dev/null +++ b/node/src/main/kotlin/net/corda/lazyhub/LazyHubImpl.kt @@ -0,0 +1,200 @@ +package net.corda.lazyhub + +import net.corda.core.internal.filterNotNull +import net.corda.core.internal.toTypedArray +import net.corda.core.internal.uncheckedCast +import net.corda.lazyhub.JConcrete.Companion.validate +import net.corda.lazyhub.KConcrete.Companion.validate +import net.corda.lazyhub.KConstructor.Companion.validate +import java.util.* +import java.util.concurrent.Callable +import java.util.stream.Stream +import kotlin.reflect.KClass +import kotlin.reflect.KFunction + +/** + * Create a new [MutableLazyHub] with no parent. + * + * Basic usage: + * * Add classes/factories/objects to the LazyHub using [MutableLazyHub.impl], [MutableLazyHub.factory] and [MutableLazyHub.obj] + * * Then ask it for a type using [LazyHub.get] and it will create (and cache) the object graph for you + * * You can use [LazyHub.getAll] to get all objects of a type, e.g. by convention pass in [Unit] to run side-effects + * + * How it works: + * * [LazyHub.get] finds the unique registered class/factory/object for the given type (or fails) + * * If it's an object, that object is returned + * * If it's a factory, it is executed with args obtained recursively from the same LazyHub + * * If it's a class, it is instantiated using a public constructor in the same way as a factory + * * Of the public constructors that can be satisfied, the one that consumes the most args is chosen + * + * Advanced usage: + * * Use an array parameter to get one-or-more args of the component type, make it nullable for zero-or-more + * * If a LazyHub can't satisfy a type (or array param) and has a parent, it asks the parent + * * Typically the root LazyHub in the hierarchy will manage all singletons of the process + */ +fun lazyHub(): MutableLazyHub = LazyHubImpl(null) + +private class SimpleProvider(override val obj: T) : Provider { + override val type get() = obj.javaClass +} + +private class LazyProvider(private val busyProviders: BusyProviders, private val underlying: Any?, override val type: Class, val chooseInvocation: () -> Callable) : Provider { + override val obj by lazy { busyProviders.runFactory(this) } + override fun toString() = underlying.toString() +} + +private class Invocation(val constructor: PublicConstructor, val argSuppliers: List>) : Callable { + fun providerCount() = argSuppliers.stream().filter { (_, supplier) -> supplier.provider != null }.count() // Allow repeated providers. + override fun call() = constructor(argSuppliers) + override fun toString() = constructor.toString() +} + +private class BusyProviders { + private val busyProviders = mutableMapOf, Callable<*>>() + fun runFactory(provider: LazyProvider): T { + if (busyProviders.contains(provider)) throw CircularDependencyException("Provider '$provider' is already busy: ${busyProviders.values}") + val invocation = provider.chooseInvocation() + busyProviders.put(provider, invocation) + try { + return invocation.call() + } finally { + busyProviders.remove(provider) + } + } +} + +private val autotypes: Map, Class<*>> = mutableMapOf, Class<*>>().apply { + Arrays::class.java.declaredMethods.filter { it.name == "hashCode" }.map { it.parameterTypes[0].componentType }.filter { it.isPrimitive }.forEach { + val boxed = java.lang.reflect.Array.get(java.lang.reflect.Array.newInstance(it, 1), 0).javaClass + put(it, boxed) + put(boxed, it) + } +} + +private infix fun Class<*>.isSatisfiedBy(clazz: Class<*>): Boolean { + return isAssignableFrom(clazz) || autotypes[this] == clazz +} + +private class LazyHubImpl(private val parent: LazyHubImpl?, private val busyProviders: BusyProviders = parent?.busyProviders ?: BusyProviders()) : MutableLazyHub { + private val providers = mutableMapOf, MutableList>>() + private fun add(provider: Provider<*>, type: Class<*> = provider.type, registered: MutableSet> = mutableSetOf()) { + if (!registered.add(type)) return + providers[type]?.add(provider) ?: providers.put(type, mutableListOf(provider)) + Stream.concat(Arrays.stream(type.interfaces), Stream.of(type.superclass, autotypes[type]).filterNotNull()).forEach { + add(provider, it, registered) + } + } + + /** The non-empty list of providers, or null. */ + private fun findProviders(clazz: Class): List>? = uncheckedCast(providers[clazz]) ?: parent?.findProviders(clazz) + + private fun dropAll(serviceClass: Class<*>) { + val removed = mutableSetOf>() + providers.iterator().run { + while (hasNext()) { + val entry = next() + if (serviceClass isSatisfiedBy entry.key) { + removed.addAll(entry.value) + remove() + } + } + } + providers.values.iterator().run { + while (hasNext()) { + val providers = next() + providers.removeAll(removed) + if (providers.isEmpty()) remove() + } + } + } + + override fun getOrNull(clazz: Class) = findProviders(clazz)?.run { (singleOrNull() ?: throw TooManyProvidersException(clazz.toString())).obj } + override fun getAll(clazz: Class) = findProviders(clazz)?.map { it.obj } ?: emptyList() + override fun child(): MutableLazyHub = LazyHubImpl(this) + override fun obj(obj: Any) = add(SimpleProvider(obj)) + override fun obj(service: KClass, obj: T) { + dropAll(service.java) + obj(obj) + } + + override fun factory(service: KClass, factory: KFunction) = factory.validate().let { + dropAll(service.java) + addFactory(it) + } + + override fun impl(service: KClass, impl: KClass) = impl.validate().let { + dropAll(service.java) + addConcrete(it) + } + + override fun impl(service: KClass, impl: Class) = impl.validate().let { + dropAll(service.java) + addConcrete(it) + } + + override fun factory(factory: KFunction<*>) = addFactory(factory.validate()) + private fun addFactory(factory: KConstructor) { + val type = factory.kFunction.returnType.toJavaType().let { if (it == Void.TYPE) Unit::class.java else it as Class<*> } + add(LazyProvider(busyProviders, factory, uncheckedCast(type)) { factory.toInvocation() }) + } + + override fun factory(lh: LazyHub, type: KClass<*>) = addFactory(lh, type) + private fun addFactory(lh: LazyHub, type: KClass) { + add(LazyProvider(busyProviders, lh, type.java) { Callable { lh[type] } }) + } + + override fun impl(impl: KClass<*>) = implGeneric(impl) + private fun implGeneric(type: KClass) = addConcrete(type.validate()) + override fun impl(impl: Class<*>) = implGeneric(impl) + private fun implGeneric(type: Class) = addConcrete(type.validate()) + private fun

> addConcrete(concrete: Concrete) { + add(LazyProvider(busyProviders, concrete, concrete.clazz) { + var fail: UnsatisfiableParamException? = null + val satisfiable = concrete.publicConstructors.mapNotNull { constructor -> + try { + constructor.toInvocation() + } catch (e: UnsatisfiableParamException) { + fail?.addSuppressed(e) ?: run { fail = e } + null + } + } + if (satisfiable.isEmpty()) throw fail!! + val greediest = mutableListOf(satisfiable[0]) + var providerCount = greediest[0].providerCount() + satisfiable.stream().skip(1).forEach next@ { + val pc = it.providerCount() + if (pc < providerCount) return@next + if (pc > providerCount) { + greediest.clear() + providerCount = pc + } + greediest += it + } + greediest.singleOrNull() ?: throw NoUniqueGreediestSatisfiableConstructorException(greediest.toString()) + }) + } + + private fun arrayProvider(arrayType: Class<*>, componentType: Class): LazyProvider>? { + val providers = findProviders(componentType) ?: return null + return LazyProvider(busyProviders, null, uncheckedCast(arrayType)) { + Callable { providers.stream().map { it.obj }.toTypedArray(componentType) } + } + } + + private fun

PublicConstructor.toInvocation() = Invocation(this, params.mapNotNull { param -> + if (param.type.isArray) { + val provider = arrayProvider(param.type, param.type.componentType) + when (provider) { + null -> param.supplierWhenUnsatisfiable()?.let { param to it } + else -> param to ArgSupplier(provider) + } + } else { + val providers = findProviders(param.type) + when (providers?.size) { + null -> param.supplierWhenUnsatisfiable()?.let { param to it } + 1 -> param to ArgSupplier(providers[0]) + else -> throw TooManyProvidersException(param.toString()) + } + } + }) +} diff --git a/node/src/main/kotlin/net/corda/lazyhub/LazyHubModel.kt b/node/src/main/kotlin/net/corda/lazyhub/LazyHubModel.kt new file mode 100644 index 0000000000..7c3217f7d7 --- /dev/null +++ b/node/src/main/kotlin/net/corda/lazyhub/LazyHubModel.kt @@ -0,0 +1,130 @@ +package net.corda.lazyhub + +import net.corda.core.internal.toMap +import net.corda.core.internal.toTypedArray +import net.corda.core.internal.uncheckedCast +import java.lang.reflect.* +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.KVisibility +import kotlin.reflect.jvm.internal.ReflectProperties +import kotlin.reflect.jvm.isAccessible + +private val javaTypeDelegateField = Class.forName("kotlin.reflect.jvm.internal.KTypeImpl").getDeclaredField("javaType\$delegate").apply { isAccessible = true } +internal fun kotlin.reflect.KType.toJavaType() = (javaTypeDelegateField.get(this) as ReflectProperties.Val<*>)() +internal interface Provider { + /** Most specific known type i.e. directly registered implementation class, or declared return type of factory method. */ + val type: Class + /** May be lazily computed. */ + val obj: T +} + +/** Like [Provider] but capable of supplying null. */ +internal class ArgSupplier(val provider: Provider<*>?) { + companion object { + val nullSupplier = ArgSupplier(null) + } + + operator fun invoke() = provider?.obj +} + +/** Common interface to Kotlin/Java params. */ +internal interface Param { + val type: Class<*> + /** The supplier, or null to supply nothing so the Kotlin default is used. */ + fun supplierWhenUnsatisfiable(): ArgSupplier? = throw (if (type.isArray) ::UnsatisfiableArrayException else ::NoSuchProviderException)(toString()) +} + +internal class KParam(val kParam: KParameter) : Param { + override val type = run { + var jType = kParam.type.toJavaType() + loop@ while (true) { + jType = when (jType) { + is ParameterizedType -> jType.rawType + is TypeVariable<*> -> jType.bounds.first() // Potentially surprising but most consistent behaviour, see unit tests. + else -> break@loop + } + } + jType as Class<*> + } + + override fun supplierWhenUnsatisfiable() = when { + kParam.isOptional -> null // Use default value, even if param is also nullable. + kParam.type.isMarkedNullable -> ArgSupplier.nullSupplier + else -> super.supplierWhenUnsatisfiable() + } + + override fun toString() = kParam.toString() +} + +internal class JParam(private val param: Parameter, private val index: Int, override val type: Class<*>) : Param { + override fun toString() = "parameter #$index ${param.name} of ${param.declaringExecutable}" +} + +internal interface PublicConstructor { + val params: List

+ operator fun invoke(argSuppliers: List>): T +} + +internal class KConstructor(val kFunction: KFunction) : PublicConstructor { + companion object { + fun KFunction.validate() = run { + if (returnType.isMarkedNullable) throw NullableReturnTypeException(toString()) + isAccessible = true + KConstructor(this) + } + } + + override val params = kFunction.parameters.map(::KParam) + override fun invoke(argSuppliers: List>): T { + return kFunction.callBy(argSuppliers.stream().map { (param, supplier) -> param.kParam to supplier() }.toMap()) + } + + override fun toString() = kFunction.toString() +} + +internal class JConstructor(private val constructor: Constructor) : PublicConstructor { + // Much cheaper to get the types up-front than via the Parameter API: + override val params = constructor.parameters.zip(constructor.parameterTypes).mapIndexed { i, (p, t) -> JParam(p, i, t) } + + override fun invoke(argSuppliers: List>): T { + return constructor.newInstance(*argSuppliers.stream().map { (_, supplier) -> supplier() }.toTypedArray()) + } + + override fun toString() = constructor.toString() +} + +internal interface Concrete> { + val clazz: Class + val publicConstructors: List +} + +internal class KConcrete private constructor(private val kClass: KClass) : Concrete> { + companion object { + fun KClass.validate() = run { + if (isAbstract) throw AbstractTypeException(toString()) + KConcrete(this).apply { + if (publicConstructors.isEmpty()) throw NoPublicConstructorsException(toString()) + } + } + } + + override val clazz get() = kClass.java + override val publicConstructors = kClass.constructors.filter { it.visibility == KVisibility.PUBLIC }.map(::KConstructor) + override fun toString() = kClass.toString() +} + +internal class JConcrete private constructor(override val clazz: Class) : Concrete> { + companion object { + fun Class.validate() = run { + if (Modifier.isAbstract(modifiers)) throw AbstractTypeException(toString()) + JConcrete(this).apply { + if (publicConstructors.isEmpty()) throw NoPublicConstructorsException(toString()) + } + } + } + + override val publicConstructors = uncheckedCast>, Array>>(clazz.constructors).map(::JConstructor) + override fun toString() = clazz.toString() +} diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 410bab983d..d17ca7cae0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -28,12 +28,14 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow +import net.corda.lazyhub.LazyHub +import net.corda.lazyhub.MutableLazyHub +import net.corda.lazyhub.lazyHub import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderInternal -import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler @@ -56,7 +58,6 @@ import net.corda.node.services.transactions.* import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager -import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.SignedNodeInfo @@ -144,9 +145,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected val runOnStop = ArrayList<() -> Any?>() private val _nodeReadyFuture = openFuture() protected var networkMapClient: NetworkMapClient? = null - - lateinit var securityManager: RPCSecurityManager get - /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ val nodeReadyFuture: CordaFuture get() = _nodeReadyFuture @@ -200,22 +198,31 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + protected open fun configure(lh: MutableLazyHub) { + // TODO: Migrate classes and factories from start method. + } + open fun start(): StartedNode { check(started == null) { "Node has already been started" } log.info("Node starting up ...") initCertificate() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) + val lh = lazyHub() + configure(lh) val identityService = makeIdentityService(identity.certificate) + lh.obj(identityService) networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) } retrieveNetworkParameters(identityService.trustRoot) // Do all of this in a database transaction so anything that might need a connection has one. val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database -> + lh.obj(database) val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries), identityService) val (keyPairs, info) = initNodeInfo(networkMapCache, identity, identityKeyPair) + lh.obj(info) identityService.loadIdentities(info.legalIdentitiesAndCerts) val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes) - val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache) + val nodeServices = makeServices(lh, keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache) val notaryService = makeNotaryService(nodeServices, database) val smm = makeStateMachineManager(database) val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader) @@ -238,13 +245,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } makeVaultObservers(schedulerService, database.hibernateConfig, smm, schemaService, flowLogicRefFactory) val rpcOps = makeRPCOps(flowStarter, database, smm) - startMessagingService(rpcOps) + lh.obj(rpcOps) + lh.getAll(Unit::class) // Run side-effects. installCoreFlows() val cordaServices = installCordaServices(flowStarter) tokenizableServices = nodeServices + cordaServices + schedulerService registerCordappFlows(smm) _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } - startShell(rpcOps) Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService) } val networkMapUpdater = NetworkMapUpdater(services.networkMapCache, @@ -280,10 +287,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, */ protected abstract fun getRxIoScheduler(): Scheduler - open fun startShell(rpcOps: CordaRPCOps) { - InteractiveShell.startShell(configuration, rpcOps, securityManager, _services.identityService, _services.database) - } - private fun initNodeInfo(networkMapCache: NetworkMapCacheBaseInternal, identity: PartyAndCertificate, identityKeyPair: KeyPair): Pair, NodeInfo> { @@ -534,7 +537,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(keyPairs: Set, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal): MutableList { + private fun makeServices(lh: LazyHub, keyPairs: Set, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal): MutableList { checkpointStorage = DBCheckpointStorage() val metrics = MetricRegistry() attachments = NodeAttachmentService(metrics) @@ -550,7 +553,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, database, info, networkMapCache) - network = makeMessagingService(database, info) + network = lh[MessagingService::class] // TODO: Retire the lateinit var. val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, services.auditService, services.monitoringService, services.networkMapCache, services.schemaService, @@ -715,9 +718,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, _started = null } - protected abstract fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService - protected abstract fun startMessagingService(rpcOps: RPCOps) - private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair { val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 16dae0ee2e..0ac13ee6d8 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -5,6 +5,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.uncheckedCast +import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub @@ -14,8 +15,10 @@ import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger +import net.corda.lazyhub.MutableLazyHub import net.corda.node.VersionInfo import net.corda.node.internal.cordapp.CordappLoader +import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.services.api.SchemaService @@ -24,6 +27,7 @@ import net.corda.node.services.config.SecurityConfiguration import net.corda.node.services.config.VerifierType import net.corda.node.services.messaging.* import net.corda.node.services.transactions.InMemoryTransactionVerifierService +import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.DemoClock @@ -133,16 +137,27 @@ open class Node(configuration: NodeConfiguration, private var messageBroker: ArtemisMessagingServer? = null private var shutdownHook: ShutdownHook? = null - - override fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService { + override fun configure(lh: MutableLazyHub) { + super.configure(lh) // Construct security manager reading users data either from the 'security' config section // if present or from rpcUsers list if the former is missing from config. - val securityManagerConfig = configuration.security?.authService ?: - SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers) + lh.obj(configuration.security?.authService ?: SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)) + lh.impl(RPCSecurityManagerImpl::class) + configuration.messagingServerAddress?.also { + lh.obj(MessagingServerAddress(it)) + } ?: run { + lh.factory(this::makeLocalMessageBroker) + } + lh.factory(this::makeMessagingService) + // Side-effects: + lh.factory(this::startMessagingService) + lh.factory(this::startShell) + } - securityManager = RPCSecurityManagerImpl(securityManagerConfig) + class MessagingServerAddress(val address: NetworkHostAndPort) - val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker() + private fun makeMessagingService(database: CordaPersistence, info: NodeInfo, messagingServerAddress: MessagingServerAddress): MessagingService { + val serverAddress = messagingServerAddress.address val advertisedAddress = info.addresses.single() printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) @@ -162,10 +177,10 @@ open class Node(configuration: NodeConfiguration, networkParameters.maxMessageSize) } - private fun makeLocalMessageBroker(): NetworkHostAndPort { + private fun makeLocalMessageBroker(securityManager: RPCSecurityManager): MessagingServerAddress { with(configuration) { messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager, networkParameters.maxMessageSize) - return NetworkHostAndPort("localhost", p2pAddress.port) + return MessagingServerAddress(NetworkHostAndPort("localhost", p2pAddress.port)) } } @@ -213,7 +228,7 @@ open class Node(configuration: NodeConfiguration, } } - override fun startMessagingService(rpcOps: RPCOps) { + private fun startMessagingService(rpcOps: RPCOps, securityManager: RPCSecurityManager) { // Start up the embedded MQ server messageBroker?.apply { runOnStop += this::stop @@ -234,6 +249,10 @@ open class Node(configuration: NodeConfiguration, } } + private fun startShell(rpcOps: CordaRPCOps, securityManager: RPCSecurityManager, identityService: IdentityService, database: CordaPersistence) { + InteractiveShell.startShell(configuration, rpcOps, securityManager, identityService, database) + } + /** * If the node is persisting to an embedded H2 database, then expose this via TCP with a DB URL of the form: * jdbc:h2:tcp://:/node diff --git a/node/src/test/kotlin/net/corda/lazyhub/LazyHubTests.kt b/node/src/test/kotlin/net/corda/lazyhub/LazyHubTests.kt new file mode 100644 index 0000000000..5d462585bc --- /dev/null +++ b/node/src/test/kotlin/net/corda/lazyhub/LazyHubTests.kt @@ -0,0 +1,640 @@ +package net.corda.lazyhub + +import net.corda.core.internal.uncheckedCast +import org.assertj.core.api.Assertions.catchThrowable +import org.hamcrest.CoreMatchers.* +import org.junit.Assert.* +import org.junit.Ignore +import org.junit.Test +import java.io.Closeable +import java.io.IOException +import java.io.Serializable +import kotlin.reflect.KFunction +import kotlin.reflect.jvm.javaConstructor +import kotlin.reflect.jvm.javaMethod +import kotlin.test.assertEquals +import kotlin.test.fail + +open class LazyHubTests { + private val lh = lazyHub() + + class Config(val info: String) + interface A + interface B { + val a: A + } + + class AImpl(val config: Config) : A + class BImpl(override val a: A) : B + class Spectator { + init { + fail("Should not be instantiated.") + } + } + + @Test + fun `basic functionality`() { + val config = Config("woo") + lh.obj(config) + lh.impl(AImpl::class) + lh.impl(BImpl::class) + lh.impl(Spectator::class) + val b = lh[B::class] + // An impl is instantiated at most once per LazyHub: + assertSame(b.a, lh[A::class]) + assertSame(b, lh[B::class]) + // More specific type to expose config without casting: + val a = lh[AImpl::class] + assertSame(b.a, a) + assertSame(config, a.config) + } + + private fun createA(config: Config): A = AImpl(config) // Declared return type is significant. + internal open fun createB(): B = fail("Should not be called.") + @Test + fun `factory works`() { + lh.obj(Config("x")) + lh.factory(this::createA) // Observe private is OK. + assertSame(AImpl::class.java, lh[A::class].javaClass) + // The factory declares A not AImpl as its return type, and lh doesn't try to be clever: + catchThrowable { lh[AImpl::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(AImpl::class.toString(), message) + } + } + + @Ignore + class Subclass : LazyHubTests() { // Should not run as tests. + @Suppress("unused") + private fun createA(@Suppress("UNUSED_PARAMETER") config: Config): A = fail("Should not be called.") + + override fun createB() = BImpl(AImpl(Config("Subclass"))) // More specific return type is OK. + } + + @Suppress("MemberVisibilityCanPrivate") + internal fun addCreateATo(lh: MutableLazyHub) { + lh.factory(this::createA) + } + + @Suppress("MemberVisibilityCanPrivate") + internal fun addCreateBTo(lh: MutableLazyHub) { + lh.factory(this::createB) + } + + @Test + fun `private factory is not virtual`() { + val baseMethod = this::createA.javaMethod!! + // Check the Subclass version would override if baseMethod wasn't private: + Subclass::class.java.getDeclaredMethod(baseMethod.name, *baseMethod.parameterTypes) + lh.obj(Config("x")) + Subclass().addCreateATo(lh) + lh[A::class] // Should not blow up. + } + + @Test + fun `non-private factory is virtual`() { + Subclass().addCreateBTo(lh) + assertEquals("Subclass", (lh[B::class].a as AImpl).config.info) // Check overridden function was called. + // The signature that was added declares B not BImpl as its return type: + catchThrowable { lh[BImpl::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(BImpl::class.toString(), message) + } + } + + private fun returnsYay() = "yay" + class TakesString(@Suppress("UNUSED_PARAMETER") text: String) + + @Test + fun `too many providers`() { + lh.obj("woo") + lh.factory(this::returnsYay) + lh.impl(TakesString::class) + catchThrowable { lh[TakesString::class] }.run { + assertSame(TooManyProvidersException::class.java, javaClass) + assertEquals(TakesString::class.constructors.single().parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(TakesString::class.qualifiedName)) + } + } + + class TakesStringOrInt(val text: String) { + @Suppress("unused") + constructor(number: Int) : this(number.toString()) + } + + @Test + fun `too many providers with alternate constructor`() { + lh.obj("woo") + lh.factory(this::returnsYay) + lh.impl(TakesStringOrInt::class) + val constructors = TakesStringOrInt::class.constructors.toList() + catchThrowable { lh[TakesStringOrInt::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(constructors[0].parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(TakesStringOrInt::class.qualifiedName)) + suppressed.single().run { + assertSame(TooManyProvidersException::class.java, javaClass) + assertEquals(constructors[1].parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(TakesStringOrInt::class.qualifiedName)) + } + } + lh.obj(123) + assertEquals("123", lh[TakesStringOrInt::class].text) + } + + @Test + fun genericClass() { + class G(val arg: T) + lh.obj("arg") + lh.impl(G::class) + assertEquals("arg", lh[G::class].arg) // Can't inspect type arg T as no such thing exists. + } + + private fun ntv(a: Y) = a.toString() + @Test + fun `nested type variable`() { + // First check it's actually legal to pass any old Closeable into the function: + val arg = Closeable {} + assertEquals(arg.toString(), ntv(arg)) + // Good, now check LazyHub can do it: + val ntv: Function1 = this::ntv + lh.factory(uncheckedCast>(ntv)) + lh.obj(arg) + assertEquals(arg.toString(), lh[String::class]) + } + + class PTWMB(val arg: Y) where Y : Closeable, Y : Serializable + private class CloseableAndSerializable : Closeable, Serializable { + override fun close() {} + } + + @Test + fun `parameter type with multiple bounds in java`() { + // At compile time we must pass something Closeable and Serializable into the constructor: + CloseableAndSerializable().let { assertSame(it, PTWMB(it).arg) } + // But at runtime only Closeable is needed (and Serializable is not enough) due to the leftmost bound erasure rule: + lh.impl(PTWMB::class.java) + lh.obj(object : Serializable {}) + catchThrowable { lh[PTWMB::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(PTWMB::class.constructors.single().javaConstructor.toString())) + } + val arg = Closeable {} + lh.obj(arg) + assertSame(arg, lh[PTWMB::class].arg) + } + + @Test + fun `parameter type with multiple bounds in kotlin`() { + lh.impl(PTWMB::class) + lh.obj(object : Serializable {}) + catchThrowable { lh[PTWMB::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(PTWMB::class.constructors.single().parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, containsString(PTWMB::class.qualifiedName)) + } + val arg = Closeable {} + lh.obj(arg) + assertSame(arg, lh[PTWMB::class].arg) + } + + private fun ptwmb(arg: Y) where Y : Closeable, Y : Serializable = arg.toString() + @Test + fun `factory parameter type with multiple bounds`() { + val ptwmb: Function1 = this::ptwmb + val kFunction = uncheckedCast>(ptwmb) + lh.factory(kFunction) + lh.obj(object : Serializable {}) + catchThrowable { lh[String::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(kFunction.parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(ptwmb.toString())) + } + val arg = Closeable {} + lh.obj(arg) + assertEquals(arg.toString(), lh[String::class]) + } + + private fun upt(a: Y) = a.toString() + @Test + fun `unbounded parameter type`() { + val upt: Function1 = this::upt + val kFunction: KFunction = uncheckedCast(upt) + lh.factory(kFunction) + // The only provider for Any is the factory, which is busy: + catchThrowable { lh[String::class] }.run { + assertSame(CircularDependencyException::class.java, javaClass) + assertThat(message, containsString("'$upt'")) + assertThat(message, endsWith(listOf(upt).toString())) + } + lh.obj(Any()) + // This time the factory isn't attempted: + catchThrowable { lh[String::class] }.run { + assertSame(TooManyProvidersException::class.java, javaClass) + assertEquals(kFunction.parameters[0].toString(), message) + assertThat(message, containsString(" #0 ")) + assertThat(message, endsWith(upt.toString())) + } + } + + open class NoPublicConstructor protected constructor() + + @Test + fun `no public constructor`() { + catchThrowable { lh.impl(NoPublicConstructor::class) }.run { + assertSame(NoPublicConstructorsException::class.java, javaClass) + assertEquals(NoPublicConstructor::class.toString(), message) + } + catchThrowable { lh.impl(NoPublicConstructor::class.java) }.run { + assertSame(NoPublicConstructorsException::class.java, javaClass) + assertEquals(NoPublicConstructor::class.toString(), message) + } + } + + private fun primitiveInt() = 1 + class IntConsumer(@Suppress("UNUSED_PARAMETER") i: Int) + class IntegerConsumer(@Suppress("UNUSED_PARAMETER") i: Int?) + + @Test + fun `boxed satisfies primitive`() { + lh.obj(1) + lh.impl(IntConsumer::class) + lh[IntConsumer::class] + } + + @Test + fun `primitive satisfies boxed`() { + lh.factory(this::primitiveInt) + lh.impl(IntegerConsumer::class.java) + lh[IntegerConsumer::class] + } + + // The primary constructor takes two distinct providers: + class TakesTwoThings(@Suppress("UNUSED_PARAMETER") first: String, @Suppress("UNUSED_PARAMETER") second: Int) { + // This constructor takes one repeated provider but we count it both times so greediness is 2: + @Suppress("unused") + constructor(first: Int, second: Int) : this(first.toString(), second) + + // This constructor would be greediest but is not satisfiable: + @Suppress("unused") + constructor(first: Int, second: String, @Suppress("UNUSED_PARAMETER") third: Config) : this(second, first) + } + + @Test + fun `equally greedy constructors kotlin`() { + lh.obj("str") + lh.obj(123) + lh.impl(TakesTwoThings::class) + catchThrowable { lh[TakesTwoThings::class] }.run { + assertSame(NoUniqueGreediestSatisfiableConstructorException::class.java, javaClass) + val expected = TakesTwoThings::class.constructors.filter { it.parameters.size == 2 } + assertEquals(2, expected.size) + assertThat(message, endsWith(expected.toString())) + } + } + + @Test + fun `equally greedy constructors java`() { + lh.obj("str") + lh.obj(123) + lh.impl(TakesTwoThings::class.java) + catchThrowable { lh[TakesTwoThings::class] }.run { + assertSame(NoUniqueGreediestSatisfiableConstructorException::class.java, javaClass) + val expected = TakesTwoThings::class.java.constructors.filter { it.parameters.size == 2 } + assertEquals(2, expected.size) + assertEquals(expected.toString(), message) + } + } + + private fun nrt(): String? = fail("Should not be invoked.") + @Test + fun `nullable return type is banned`() { + catchThrowable { lh.factory(this::nrt) }.run { + assertSame(NullableReturnTypeException::class.java, javaClass) + assertThat(message, endsWith(this@LazyHubTests::nrt.toString())) + } + } + + @Test + fun unsatisfiableArrayParam() { + class Impl(@Suppress("UNUSED_PARAMETER") v: Array) + lh.impl(Impl::class) + catchThrowable { lh[Impl::class] }.run { + assertSame(UnsatisfiableArrayException::class.java, javaClass) + assertEquals(Impl::class.constructors.single().parameters[0].toString(), message) + } + // Arrays are only special in real params, you should use getAll to get all the Strings: + catchThrowable { lh[Array::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(Array::class.java.toString(), message) + } + assertEquals(emptyList(), lh.getAll(String::class)) + } + + @Test + fun arrayParam1() { + class Impl(val v: Array) + lh.impl(Impl::class) + lh.obj("a") + assertArrayEquals(arrayOf("a"), lh[Impl::class].v) + } + + @Test + fun arrayParam2() { + class Impl(val v: Array) + lh.impl(Impl::class) + lh.obj("y") + lh.obj("x") + assertArrayEquals(arrayOf("y", "x"), lh[Impl::class].v) + } + + @Test + fun nullableArrayParam() { + class Impl(val v: Array?) + lh.impl(Impl::class) + assertEquals(null, lh[Impl::class].v) + } + + @Test + fun arraysAreNotCached() { + class B(val v: Array) + class A(val v: Array, val b: B) + class C(val v: Array) + class D(val v: Array) + lh.obj("x") + lh.obj("y") + lh.impl(A::class) + lh.impl(B::class) + val a = lh[A::class] + a.run { + assertArrayEquals(arrayOf("x", "y"), v) + assertArrayEquals(arrayOf("x", "y"), b.v) + assertNotSame(v, b.v) + } + assertSame(lh[B::class].v, a.b.v) // Because it's the same (cached) instance of B. + lh.impl(C::class) + lh[C::class].run { + assertArrayEquals(arrayOf("x", "y"), v) + assertNotSame(v, a.v) + assertNotSame(v, a.b.v) + } + lh.obj("z") + lh.impl(D::class) + lh[D::class].run { + assertArrayEquals(arrayOf("x", "y", "z"), v) + } + } + + class C1(@Suppress("UNUSED_PARAMETER") c2: C2) + class C2(@Suppress("UNUSED_PARAMETER") c3: String) + + private fun c3(@Suppress("UNUSED_PARAMETER") c2: C2): String { + fail("Should not be called.") + } + + @Test + fun `circularity error kotlin`() { + lh.impl(C1::class) + lh.impl(C2::class) + lh.factory(this::c3) + catchThrowable { lh[C1::class] }.run { + assertSame(CircularDependencyException::class.java, javaClass) + assertThat(message, containsString("'${C2::class}'")) + assertThat(message, endsWith(listOf(C1::class.constructors.single(), C2::class.constructors.single(), this@LazyHubTests::c3).toString())) + } + } + + @Test + fun `circularity error java`() { + lh.impl(C1::class.java) + lh.impl(C2::class.java) + lh.factory(this::c3) + catchThrowable { lh[C1::class] }.run { + assertSame(CircularDependencyException::class.java, javaClass) + assertThat(message, containsString("'${C2::class}'")) + assertThat(message, endsWith(listOf(C1::class.constructors.single().javaConstructor, C2::class.constructors.single().javaConstructor, this@LazyHubTests::c3).toString())) + } + } + + @Test + fun `ancestor hub providers are visible`() { + val c = Config("over here") + lh.obj(c) + lh.child().also { + it.impl(AImpl::class) + assertSame(c, it[AImpl::class].config) + } + lh.child().child().also { + it.impl(AImpl::class) + assertSame(c, it[AImpl::class].config) + } + } + + @Test + fun `descendant hub providers are not visible`() { + val child = lh.child() + child.obj(Config("over here")) + lh.impl(AImpl::class) + catchThrowable { lh[AImpl::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(AImpl::class.constructors.single().parameters.single().toString(), message) + } + // Fails even though we go via the child, as the cached AImpl in lh shouldn't have collaborators from descendant hubs: + catchThrowable { child[AImpl::class] }.run { + assertSame(NoSuchProviderException::class.java, javaClass) + assertEquals(AImpl::class.constructors.single().parameters.single().toString(), message) + } + } + + class AllConfigs(val configs: Array) + + @Test + fun `nearest ancestor with at least one provider wins`() { + lh.obj(Config("deep")) + lh.child().also { + it.child().also { + it.impl(AllConfigs::class) + assertEquals(listOf("deep"), it[AllConfigs::class].configs.map { it.info }) + } + it.obj(Config("shallow1")) + it.obj(Config("shallow2")) + it.child().also { + it.impl(AllConfigs::class) + assertEquals(listOf("shallow1", "shallow2"), it[AllConfigs::class].configs.map { it.info }) + } + it.child().also { + it.obj(Config("local")) + it.impl(AllConfigs::class) + assertEquals(listOf("local"), it[AllConfigs::class].configs.map { it.info }) + } + } + } + + @Test + fun `abstract type`() { + catchThrowable { lh.impl(Runnable::class) }.run { + assertSame(AbstractTypeException::class.java, javaClass) + assertEquals(Runnable::class.toString(), message) + } + catchThrowable { lh.impl(Runnable::class.java) }.run { + assertSame(AbstractTypeException::class.java, javaClass) + assertEquals(Runnable::class.java.toString(), message) + } + } + + private interface Service + open class GoodService : Service + abstract class BadService1 : Service + class BadService2 private constructor() : Service + + private fun badService3(): Service? = fail("Should not be called.") + @Test + fun `existing providers not removed if new type is bad`() { + lh.impl(GoodService::class) + catchThrowable { lh.impl(Service::class, BadService1::class) }.run { + assertSame(AbstractTypeException::class.java, javaClass) + assertEquals(BadService1::class.toString(), message) + } + catchThrowable { lh.impl(Service::class, BadService2::class) }.run { + assertSame(NoPublicConstructorsException::class.java, javaClass) + assertEquals(BadService2::class.toString(), message) + } + catchThrowable { lh.impl(Service::class, BadService2::class.java) }.run { + assertSame(NoPublicConstructorsException::class.java, javaClass) + assertEquals(BadService2::class.toString(), message) + } + // Type system won't let you pass in badService3, but I still want validation up-front: + catchThrowable { lh.factory(Service::class, uncheckedCast(this::badService3)) }.run { + assertSame(NullableReturnTypeException::class.java, javaClass) + assertEquals(this@LazyHubTests::badService3.toString(), message) + } + assertSame(GoodService::class.java, lh[Service::class].javaClass) + } + + class GoodService2 : GoodService() + + @Test + fun `service providers are removed completely`() { + lh.impl(GoodService::class) + assertSame(GoodService::class.java, lh[Service::class].javaClass) + lh.impl(GoodService::class, GoodService2::class) + // In particular, GoodService is no longer registered against Service (or Any): + assertSame(GoodService2::class.java, lh[Service::class].javaClass) + assertSame(GoodService2::class.java, lh[Any::class].javaClass) + } + + class JParamExample(@Suppress("UNUSED_PARAMETER") str: String, @Suppress("UNUSED_PARAMETER") num: Int) + + @Test + fun `JParam has useful toString`() { + val c = JParamExample::class.java.constructors.single() + // Parameter doesn't expose its index, here we deliberately pass in the wrong one to see what happens: + val text = JParam(c.parameters[0], 1, IOException::class.java).toString() + assertThat(text, containsString(" #1 ")) + assertThat(text, anyOf(containsString(" str "), containsString(" arg0 "))) + assertThat(text, endsWith(c.toString())) + } + + private val sideEffects = mutableListOf() + private fun sideEffect1() { + sideEffects.add(1) + } + + private fun sideEffect2() { + sideEffects.add(2) + } + + @Test + fun `side-effects are idempotent as a consequence of caching of results`() { + lh.factory(this::sideEffect1) + assertEquals(listOf(Unit), lh.getAll(Unit::class)) + assertEquals(listOf(1), sideEffects) + lh.factory(this::sideEffect2) + assertEquals(listOf(Unit, Unit), lh.getAll(Unit::class)) // Get both results. + assertEquals(listOf(1, 2), sideEffects) // sideEffect1 didn't run again. + } + + @Test + fun `getAll returns empty list when there is nothing to return`() { + // This is in contrast to the exception thrown by an array param, which would not be useful to replicate here: + assertEquals(emptyList(), lh.getAll(IOException::class)) + } + + // Two params needed to make primary constructor the winner when both are satisfiable. + // It's probably true that the secondary will always trigger a CircularDependencyException, but LazyHub isn't clever enough to tell. + class InvocationSwitcher(@Suppress("UNUSED_PARAMETER") s: String, @Suppress("UNUSED_PARAMETER") t: String) { + @Suppress("unused") + constructor(same: InvocationSwitcher) : this(same.toString(), same.toString()) + } + + @Test + fun `chosen constructor is not set in stone`() { + lh.impl(InvocationSwitcher::class) + assertSame(CircularDependencyException::class.java, catchThrowable { lh[InvocationSwitcher::class] }.javaClass) + lh.obj("alt") + lh[InvocationSwitcher::class] // Succeeds via other constructor. + } + + class GreedinessUnits(@Suppress("UNUSED_PARAMETER") v: Array, @Suppress("UNUSED_PARAMETER") z: Int) { + // Two greediness units even though it's one provider repeated: + @Suppress("unused") + constructor(z1: Int, z2: Int) : this(emptyArray(), z1 + z2) + } + + @Test + fun `array param counts as one greediness unit`() { + lh.obj("x") + lh.obj("y") + lh.obj(100) + lh.impl(GreedinessUnits::class) + assertSame(NoUniqueGreediestSatisfiableConstructorException::class.java, catchThrowable { lh[GreedinessUnits::class] }.javaClass) + } + + interface TriangleBase + interface TriangleSide : TriangleBase + class TriangleImpl : TriangleBase, TriangleSide + + @Test + fun `provider registered exactly once against each supertype`() { + lh.impl(TriangleImpl::class) + lh[TriangleBase::class] // Don't throw TooManyProvidersException. + } + + interface Service1 + interface Service2 + class ServiceImpl1 : Service1, Service2 + class ServiceImpl2 : Service2 + + @Test + fun `do not leak empty provider list`() { + lh.impl(ServiceImpl1::class) + lh.impl(Service2::class, ServiceImpl2::class) + assertSame(NoSuchProviderException::class.java, catchThrowable { lh[Service1::class] }.javaClass) + } + + class Global + class Session(val global: Global, val local: Int) + + @Test + fun `child can be used to create a scope`() { + lh.impl(Global::class) + lh.factory(lh.child().also { + it.obj(1) + it.impl(Session::class) + }, Session::class) + lh.factory(lh.child().also { + it.obj(2) + it.impl(Session::class) + }, Session::class) + val sessions = lh.getAll(Session::class) + val g = lh[Global::class] + sessions.forEach { assertSame(g, it.global) } + assertEquals(listOf(1, 2), sessions.map { it.local }) + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 0b8ae32540..7e8aa8225f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -5,7 +5,6 @@ import com.google.common.jimfs.Jimfs import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.DoNotImplement -import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.Crypto import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name @@ -15,17 +14,15 @@ import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectory import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients -import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.NodeInfo import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SerializationWhitelist import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.seconds +import net.corda.lazyhub.MutableLazyHub import net.corda.node.VersionInfo import net.corda.node.internal.AbstractNode import net.corda.node.internal.StartedNode @@ -294,9 +291,14 @@ open class MockNetwork(private val cordappPackages: List, } } + override fun configure(lh: MutableLazyHub) { + super.configure(lh) + lh.factory(this::makeMessagingService) + } + // We only need to override the messaging service here, as currently everything that hits disk does so // through the java.nio API which we are already mocking via Jimfs. - override fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService { + private fun makeMessagingService(database: CordaPersistence): MessagingService { require(id >= 0) { "Node ID must be zero or positive, was passed: " + id } return mockNet.messagingNetwork.createNodeWithID( !mockNet.threadPerNode, @@ -315,14 +317,6 @@ open class MockNetwork(private val cordappPackages: List, return E2ETestKeyManagementService(identityService, keyPairs) } - override fun startShell(rpcOps: CordaRPCOps) { - //No mock shell - } - - override fun startMessagingService(rpcOps: RPCOps) { - // Nothing to do - } - // This is not thread safe, but node construction is done on a single thread, so that should always be fine override fun generateKeyPair(): KeyPair { counter = counter.add(BigInteger.ONE)