mirror of
https://github.com/corda/corda.git
synced 2025-01-03 03:36:48 +00:00
Move the implementation of the FlowLogicRefFactory internal to the node as it is an implementation detail, not an API.
This commit is contained in:
parent
489661a289
commit
bfa7d50d37
@ -1,194 +1,30 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import com.google.common.primitives.Primitives
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
|
||||||
import java.lang.reflect.ParameterizedType
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.reflect.KFunction
|
|
||||||
import kotlin.reflect.KParameter
|
|
||||||
import kotlin.reflect.jvm.javaConstructor
|
|
||||||
import kotlin.reflect.jvm.javaType
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for conversion to and from [FlowLogic] and [FlowLogicRef] instances.
|
* The public factory interface for creating validated FlowLogicRef instances as part of the scheduling framework.
|
||||||
*
|
* Typically this would be used from within the nextScheduledActivity method of a QueryableState to specify
|
||||||
* Validation of types is performed on the way in and way out in case this object is passed between JVMs which might have differing
|
* the flow to run at the scheduled time.
|
||||||
* whitelists.
|
|
||||||
*
|
|
||||||
* TODO: Ways to populate whitelist of "blessed" flows per node/party
|
|
||||||
* TODO: Ways to populate argument types whitelist. Per node/party or global?
|
|
||||||
* TODO: Align with API related logic for passing in FlowLogic references (FlowRef)
|
|
||||||
* TODO: Actual support for AppContext / AttachmentsClassLoader
|
|
||||||
*/
|
*/
|
||||||
class FlowLogicRefFactory(val flowWhitelist: Map<String, Set<String>>) : SingletonSerializeAsToken() {
|
interface FlowLogicRefFactory {
|
||||||
constructor() : this(mapOf())
|
fun create(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||||
|
|
||||||
// Pending real dependence on AppContext for class loading etc
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
private fun validateFlowClassName(className: String, appContext: AppContext) {
|
|
||||||
// TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check
|
|
||||||
require(flowWhitelist.containsKey(className)) { "${FlowLogic::class.java.simpleName} of ${FlowLogicRef::class.java.simpleName} must have type on the whitelist: $className" }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pending real dependence on AppContext for class loading etc
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
private fun validateArgClassName(className: String, argClassName: String, appContext: AppContext) {
|
|
||||||
// TODO: consider more carefully what to whitelist and how to secure flows
|
|
||||||
// For now automatically accept standard java.lang.* and kotlin.* types.
|
|
||||||
// All other types require manual specification at FlowLogicRefFactory construction time.
|
|
||||||
if (argClassName.startsWith("java.lang.") || argClassName.startsWith("kotlin.")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check
|
|
||||||
require(flowWhitelist[className]!!.contains(argClassName)) { "Args to $className must have types on the args whitelist: $argClassName, but it has ${flowWhitelist[className]}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [FlowLogicRef] for the Kotlin primary constructor of a named [FlowLogic]
|
|
||||||
*/
|
|
||||||
fun createKotlin(flowLogicClassName: String, args: Map<String, Any?>, attachments: List<SecureHash> = emptyList()): FlowLogicRef {
|
|
||||||
val context = AppContext(attachments)
|
|
||||||
validateFlowClassName(flowLogicClassName, context)
|
|
||||||
for (arg in args.values.filterNotNull()) {
|
|
||||||
validateArgClassName(flowLogicClassName, arg.javaClass.name, context)
|
|
||||||
}
|
|
||||||
val clazz = Class.forName(flowLogicClassName)
|
|
||||||
require(FlowLogic::class.java.isAssignableFrom(clazz)) { "$flowLogicClassName is not a FlowLogic" }
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val logic = clazz as Class<FlowLogic<FlowLogic<*>>>
|
|
||||||
return createKotlin(logic, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [FlowLogicRef] by assuming a single constructor and the given args.
|
|
||||||
*/
|
|
||||||
fun create(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
|
||||||
// TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly
|
|
||||||
// to avoid requiring only a single constructor.
|
|
||||||
val argTypes = args.map { it?.javaClass }
|
|
||||||
val constructor = try {
|
|
||||||
type.kotlin.constructors.single { ctor ->
|
|
||||||
// Get the types of the arguments, always boxed (as that's what we get in the invocation).
|
|
||||||
val ctorTypes = ctor.javaConstructor!!.parameterTypes.map { Primitives.wrap(it) }
|
|
||||||
if (argTypes.size != ctorTypes.size)
|
|
||||||
return@single false
|
|
||||||
for ((argType, ctorType) in argTypes.zip(ctorTypes)) {
|
|
||||||
if (argType == null) continue // Try and find a match based on the other arguments.
|
|
||||||
if (!ctorType.isAssignableFrom(argType)) return@single false
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
throw IllegalFlowLogicException(type, "due to ambiguous match against the constructors: $argTypes")
|
|
||||||
} catch (e: NoSuchElementException) {
|
|
||||||
throw IllegalFlowLogicException(type, "due to missing constructor for arguments: $argTypes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build map of args from array
|
|
||||||
val argsMap = args.zip(constructor.parameters).map { Pair(it.second.name!!, it.first) }.toMap()
|
|
||||||
return createKotlin(type, argsMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [FlowLogicRef] by trying to find a Kotlin constructor that matches the given args.
|
|
||||||
*
|
|
||||||
* TODO: Rethink language specific naming.
|
|
||||||
*/
|
|
||||||
fun createKotlin(type: Class<out FlowLogic<*>>, args: Map<String, Any?>): FlowLogicRef {
|
|
||||||
// TODO: we need to capture something about the class loader or "application context" into the ref,
|
|
||||||
// perhaps as some sort of ThreadLocal style object. For now, just create an empty one.
|
|
||||||
val appContext = AppContext(emptyList())
|
|
||||||
validateFlowClassName(type.name, appContext)
|
|
||||||
// Check we can find a constructor and populate the args to it, but don't call it
|
|
||||||
createConstructor(appContext, type, args)
|
|
||||||
return FlowLogicRef(type.name, appContext, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [FlowLogicRef] by trying to find a Java constructor that matches the given args.
|
|
||||||
*/
|
|
||||||
private fun createJava(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
|
||||||
// Build map for each
|
|
||||||
val argsMap = HashMap<String, Any?>(args.size)
|
|
||||||
var index = 0
|
|
||||||
args.forEach { argsMap["arg${index++}"] = it }
|
|
||||||
return createKotlin(type, argsMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
|
|
||||||
validateFlowClassName(ref.flowLogicClassName, ref.appContext)
|
|
||||||
val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java)
|
|
||||||
return createConstructor(ref.appContext, klass, ref.args)()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createConstructor(appContext: AppContext, clazz: Class<out FlowLogic<*>>, args: Map<String, Any?>): () -> FlowLogic<*> {
|
|
||||||
for (constructor in clazz.kotlin.constructors) {
|
|
||||||
val params = buildParams(appContext, clazz, constructor, args) ?: continue
|
|
||||||
// If we get here then we matched every parameter
|
|
||||||
return { constructor.callBy(params) }
|
|
||||||
}
|
|
||||||
throw IllegalFlowLogicException(clazz, "as could not find matching constructor for: $args")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildParams(appContext: AppContext, clazz: Class<out FlowLogic<*>>, constructor: KFunction<FlowLogic<*>>, args: Map<String, Any?>): HashMap<KParameter, Any?>? {
|
|
||||||
val params = hashMapOf<KParameter, Any?>()
|
|
||||||
val usedKeys = hashSetOf<String>()
|
|
||||||
for (parameter in constructor.parameters) {
|
|
||||||
if (!tryBuildParam(args, parameter, params)) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
usedKeys += parameter.name!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((args.keys - usedKeys).isNotEmpty()) {
|
|
||||||
// Not all args were used
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
params.values.forEach { if (it is Any) validateArgClassName(clazz.name, it.javaClass.name, appContext) }
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryBuildParam(args: Map<String, Any?>, parameter: KParameter, params: HashMap<KParameter, Any?>): Boolean {
|
|
||||||
val containsKey = parameter.name in args
|
|
||||||
// OK to be missing if optional
|
|
||||||
return (parameter.isOptional && !containsKey) || (containsKey && paramCanBeBuilt(args, parameter, params))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun paramCanBeBuilt(args: Map<String, Any?>, parameter: KParameter, params: HashMap<KParameter, Any?>): Boolean {
|
|
||||||
val value = args[parameter.name]
|
|
||||||
params[parameter] = value
|
|
||||||
return (value is Any && parameterAssignableFrom(parameter.type.javaType, value)) || parameter.type.isMarkedNullable
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parameterAssignableFrom(type: Type, value: Any): Boolean {
|
|
||||||
if (type is Class<*>) {
|
|
||||||
if (type.isPrimitive) {
|
|
||||||
return Primitives.unwrap(value.javaClass) == type
|
|
||||||
} else {
|
|
||||||
return type.isAssignableFrom(value.javaClass)
|
|
||||||
}
|
|
||||||
} else if (type is ParameterizedType) {
|
|
||||||
return parameterAssignableFrom(type.rawType, value)
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg")
|
class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox.
|
* A handle interface representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox.
|
||||||
|
* Use FlowLogicRefFactory to construct a concrete security checked instance.
|
||||||
*
|
*
|
||||||
* Only allows a String reference to the FlowLogic class, and only allows restricted argument types as per [FlowLogicRefFactory].
|
* Only allows a String reference to the FlowLogic class, and only allows restricted argument types as per [FlowLogicRefFactory].
|
||||||
*/
|
*/
|
||||||
// TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes)
|
// TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes)
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class FlowLogicRef internal constructor(val flowLogicClassName: String, val appContext: AppContext, val args: Map<String, Any?>)
|
interface FlowLogicRef
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is just some way to track what attachments need to be in the class loader, but may later include some app
|
* This is just some way to track what attachments need to be in the class loader, but may later include some app
|
||||||
|
@ -12,7 +12,6 @@ import net.corda.core.crypto.Party
|
|||||||
import net.corda.core.crypto.X509Utilities
|
import net.corda.core.crypto.X509Utilities
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.FlowInitiator
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
|
||||||
import net.corda.core.flows.FlowVersion
|
import net.corda.core.flows.FlowVersion
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.messaging.RPCOps
|
import net.corda.core.messaging.RPCOps
|
||||||
@ -46,6 +45,7 @@ import net.corda.node.services.network.PersistentNetworkMapService
|
|||||||
import net.corda.node.services.persistence.*
|
import net.corda.node.services.persistence.*
|
||||||
import net.corda.node.services.schema.HibernateObserver
|
import net.corda.node.services.schema.HibernateObserver
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
import net.corda.node.services.schema.NodeSchemaService
|
||||||
|
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
import net.corda.node.services.statemachine.StateMachineManager
|
||||||
import net.corda.node.services.statemachine.flowVersion
|
import net.corda.node.services.statemachine.flowVersion
|
||||||
@ -134,7 +134,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
|
|
||||||
// Internal only
|
// Internal only
|
||||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||||
override val flowLogicRefFactory: FlowLogicRefFactory get() = flowLogicFactory
|
override val flowLogicRefFactory: FlowLogicRefFactoryInternal get() = flowLogicFactory
|
||||||
|
|
||||||
override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> {
|
override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> {
|
||||||
return serverThread.fetchFrom { smm.add(logic, flowInitiator) }
|
return serverThread.fetchFrom { smm.add(logic, flowInitiator) }
|
||||||
@ -173,7 +173,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
lateinit var net: MessagingService
|
lateinit var net: MessagingService
|
||||||
lateinit var netMapCache: NetworkMapCacheInternal
|
lateinit var netMapCache: NetworkMapCacheInternal
|
||||||
lateinit var scheduler: NodeSchedulerService
|
lateinit var scheduler: NodeSchedulerService
|
||||||
lateinit var flowLogicFactory: FlowLogicRefFactory
|
lateinit var flowLogicFactory: FlowLogicRefFactoryInternal
|
||||||
lateinit var schemas: SchemaService
|
lateinit var schemas: SchemaService
|
||||||
val customServices: ArrayList<Any> = ArrayList()
|
val customServices: ArrayList<Any> = ArrayList()
|
||||||
protected val runOnStop: ArrayList<Runnable> = ArrayList()
|
protected val runOnStop: ArrayList<Runnable> = ArrayList()
|
||||||
@ -381,7 +381,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initialiseFlowLogicFactory(): FlowLogicRefFactory {
|
private fun initialiseFlowLogicFactory(): FlowLogicRefFactoryInternal {
|
||||||
val flowWhitelist = HashMap<String, Set<String>>()
|
val flowWhitelist = HashMap<String, Set<String>>()
|
||||||
|
|
||||||
for ((flowClass, extraArgumentTypes) in defaultFlowWhiteList) {
|
for ((flowClass, extraArgumentTypes) in defaultFlowWhiteList) {
|
||||||
@ -398,7 +398,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return FlowLogicRefFactory(flowWhitelist)
|
return FlowLogicRefFactoryImpl(flowWhitelist)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makePluginServices(tokenizableServices: MutableList<Any>): List<Any> {
|
private fun makePluginServices(tokenizableServices: MutableList<Any>): List<Any> {
|
||||||
|
@ -2,10 +2,7 @@ package net.corda.node.services.api
|
|||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting
|
import com.google.common.annotations.VisibleForTesting
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
|
||||||
import net.corda.core.flows.FlowStateMachine
|
|
||||||
import net.corda.core.messaging.SingleMessageRecipient
|
import net.corda.core.messaging.SingleMessageRecipient
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.PluginServiceHub
|
import net.corda.core.node.PluginServiceHub
|
||||||
@ -50,6 +47,11 @@ interface NetworkMapCacheInternal : NetworkMapCache {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FlowLogicRefFactoryInternal : FlowLogicRefFactory {
|
||||||
|
val flowWhitelist: Map<String, Set<String>>
|
||||||
|
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*>
|
||||||
|
}
|
||||||
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
sealed class NetworkCacheError : Exception() {
|
sealed class NetworkCacheError : Exception() {
|
||||||
/** Indicates a failure to deregister, because of a rejected request from the remote node */
|
/** Indicates a failure to deregister, because of a rejected request from the remote node */
|
||||||
@ -62,7 +64,7 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract val monitoringService: MonitoringService
|
abstract val monitoringService: MonitoringService
|
||||||
abstract val flowLogicRefFactory: FlowLogicRefFactory
|
abstract val flowLogicRefFactory: FlowLogicRefFactoryInternal
|
||||||
abstract val schemaService: SchemaService
|
abstract val schemaService: SchemaService
|
||||||
abstract override val networkMapCache: NetworkMapCacheInternal
|
abstract override val networkMapCache: NetworkMapCacheInternal
|
||||||
abstract val schedulerService: SchedulerService
|
abstract val schedulerService: SchedulerService
|
||||||
|
@ -9,11 +9,11 @@ import net.corda.core.contracts.ScheduledStateRef
|
|||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.FlowInitiator
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
|
import net.corda.node.services.api.FlowLogicRefFactoryInternal
|
||||||
import net.corda.node.services.api.SchedulerService
|
import net.corda.node.services.api.SchedulerService
|
||||||
import net.corda.node.services.api.ServiceHubInternal
|
import net.corda.node.services.api.ServiceHubInternal
|
||||||
import net.corda.node.utilities.*
|
import net.corda.node.utilities.*
|
||||||
@ -44,7 +44,7 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class NodeSchedulerService(private val services: ServiceHubInternal,
|
class NodeSchedulerService(private val services: ServiceHubInternal,
|
||||||
private val flowLogicRefFactory: FlowLogicRefFactory,
|
private val flowLogicRefFactory: FlowLogicRefFactoryInternal,
|
||||||
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
|
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
|
||||||
private val unfinishedSchedules: ReusableLatch = ReusableLatch())
|
private val unfinishedSchedules: ReusableLatch = ReusableLatch())
|
||||||
: SchedulerService, SingletonSerializeAsToken() {
|
: SchedulerService, SingletonSerializeAsToken() {
|
||||||
|
@ -0,0 +1,191 @@
|
|||||||
|
package net.corda.node.services.statemachine
|
||||||
|
|
||||||
|
import com.google.common.primitives.Primitives
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.AppContext
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.FlowLogicRef
|
||||||
|
import net.corda.core.flows.IllegalFlowLogicException
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
|
import net.corda.node.services.api.FlowLogicRefFactoryInternal
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.reflect.KFunction
|
||||||
|
import kotlin.reflect.KParameter
|
||||||
|
import kotlin.reflect.jvm.javaConstructor
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal concrete implementation of the FlowLogicRef marker interface.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, val appContext: AppContext, val args: Map<String, Any?>) : FlowLogicRef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for conversion to and from [FlowLogic] and [FlowLogicRef] instances.
|
||||||
|
*
|
||||||
|
* Validation of types is performed on the way in and way out in case this object is passed between JVMs which might have differing
|
||||||
|
* whitelists.
|
||||||
|
*
|
||||||
|
* TODO: Ways to populate whitelist of "blessed" flows per node/party
|
||||||
|
* TODO: Ways to populate argument types whitelist. Per node/party or global?
|
||||||
|
* TODO: Align with API related logic for passing in FlowLogic references (FlowRef)
|
||||||
|
* TODO: Actual support for AppContext / AttachmentsClassLoader
|
||||||
|
*/
|
||||||
|
class FlowLogicRefFactoryImpl(override val flowWhitelist: Map<String, Set<String>>) : SingletonSerializeAsToken(), FlowLogicRefFactoryInternal {
|
||||||
|
constructor() : this(mapOf())
|
||||||
|
|
||||||
|
// Pending real dependence on AppContext for class loading etc
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
private fun validateFlowClassName(className: String, appContext: AppContext) {
|
||||||
|
// TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check
|
||||||
|
require(flowWhitelist.containsKey(className)) { "${FlowLogic::class.java.simpleName} of ${FlowLogicRef::class.java.simpleName} must have type on the whitelist: $className" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pending real dependence on AppContext for class loading etc
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
private fun validateArgClassName(className: String, argClassName: String, appContext: AppContext) {
|
||||||
|
// TODO: consider more carefully what to whitelist and how to secure flows
|
||||||
|
// For now automatically accept standard java.lang.* and kotlin.* types.
|
||||||
|
// All other types require manual specification at FlowLogicRefFactory construction time.
|
||||||
|
if (argClassName.startsWith("java.lang.") || argClassName.startsWith("kotlin.")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check
|
||||||
|
require(flowWhitelist[className]!!.contains(argClassName)) { "Args to $className must have types on the args whitelist: $argClassName, but it has ${flowWhitelist[className]}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [FlowLogicRef] for the Kotlin primary constructor of a named [FlowLogic]
|
||||||
|
*/
|
||||||
|
fun createKotlin(flowLogicClassName: String, args: Map<String, Any?>, attachments: List<SecureHash> = emptyList()): FlowLogicRef {
|
||||||
|
val context = AppContext(attachments)
|
||||||
|
validateFlowClassName(flowLogicClassName, context)
|
||||||
|
for (arg in args.values.filterNotNull()) {
|
||||||
|
validateArgClassName(flowLogicClassName, arg.javaClass.name, context)
|
||||||
|
}
|
||||||
|
val clazz = Class.forName(flowLogicClassName)
|
||||||
|
require(FlowLogic::class.java.isAssignableFrom(clazz)) { "$flowLogicClassName is not a FlowLogic" }
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val logic = clazz as Class<FlowLogic<FlowLogic<*>>>
|
||||||
|
return createKotlin(logic, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [FlowLogicRef] by assuming a single constructor and the given args.
|
||||||
|
*/
|
||||||
|
override fun create(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||||
|
// TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly
|
||||||
|
// to avoid requiring only a single constructor.
|
||||||
|
val argTypes = args.map { it?.javaClass }
|
||||||
|
val constructor = try {
|
||||||
|
type.kotlin.constructors.single { ctor ->
|
||||||
|
// Get the types of the arguments, always boxed (as that's what we get in the invocation).
|
||||||
|
val ctorTypes = ctor.javaConstructor!!.parameterTypes.map { Primitives.wrap(it) }
|
||||||
|
if (argTypes.size != ctorTypes.size)
|
||||||
|
return@single false
|
||||||
|
for ((argType, ctorType) in argTypes.zip(ctorTypes)) {
|
||||||
|
if (argType == null) continue // Try and find a match based on the other arguments.
|
||||||
|
if (!ctorType.isAssignableFrom(argType)) return@single false
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw IllegalFlowLogicException(type, "due to ambiguous match against the constructors: $argTypes")
|
||||||
|
} catch (e: NoSuchElementException) {
|
||||||
|
throw IllegalFlowLogicException(type, "due to missing constructor for arguments: $argTypes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build map of args from array
|
||||||
|
val argsMap = args.zip(constructor.parameters).map { Pair(it.second.name!!, it.first) }.toMap()
|
||||||
|
return createKotlin(type, argsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [FlowLogicRef] by trying to find a Kotlin constructor that matches the given args.
|
||||||
|
*
|
||||||
|
* TODO: Rethink language specific naming.
|
||||||
|
*/
|
||||||
|
fun createKotlin(type: Class<out FlowLogic<*>>, args: Map<String, Any?>): FlowLogicRef {
|
||||||
|
// TODO: we need to capture something about the class loader or "application context" into the ref,
|
||||||
|
// perhaps as some sort of ThreadLocal style object. For now, just create an empty one.
|
||||||
|
val appContext = AppContext(emptyList())
|
||||||
|
validateFlowClassName(type.name, appContext)
|
||||||
|
// Check we can find a constructor and populate the args to it, but don't call it
|
||||||
|
createConstructor(appContext, type, args)
|
||||||
|
return FlowLogicRefImpl(type.name, appContext, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [FlowLogicRef] by trying to find a Java constructor that matches the given args.
|
||||||
|
*/
|
||||||
|
private fun createJava(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||||
|
// Build map for each
|
||||||
|
val argsMap = HashMap<String, Any?>(args.size)
|
||||||
|
var index = 0
|
||||||
|
args.forEach { argsMap["arg${index++}"] = it }
|
||||||
|
return createKotlin(type, argsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
|
||||||
|
if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface")
|
||||||
|
validateFlowClassName(ref.flowLogicClassName, ref.appContext)
|
||||||
|
val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java)
|
||||||
|
return createConstructor(ref.appContext, klass, ref.args)()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createConstructor(appContext: AppContext, clazz: Class<out FlowLogic<*>>, args: Map<String, Any?>): () -> FlowLogic<*> {
|
||||||
|
for (constructor in clazz.kotlin.constructors) {
|
||||||
|
val params = buildParams(appContext, clazz, constructor, args) ?: continue
|
||||||
|
// If we get here then we matched every parameter
|
||||||
|
return { constructor.callBy(params) }
|
||||||
|
}
|
||||||
|
throw IllegalFlowLogicException(clazz, "as could not find matching constructor for: $args")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildParams(appContext: AppContext, clazz: Class<out FlowLogic<*>>, constructor: KFunction<FlowLogic<*>>, args: Map<String, Any?>): HashMap<KParameter, Any?>? {
|
||||||
|
val params = hashMapOf<KParameter, Any?>()
|
||||||
|
val usedKeys = hashSetOf<String>()
|
||||||
|
for (parameter in constructor.parameters) {
|
||||||
|
if (!tryBuildParam(args, parameter, params)) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
usedKeys += parameter.name!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((args.keys - usedKeys).isNotEmpty()) {
|
||||||
|
// Not all args were used
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
params.values.forEach { if (it is Any) validateArgClassName(clazz.name, it.javaClass.name, appContext) }
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryBuildParam(args: Map<String, Any?>, parameter: KParameter, params: HashMap<KParameter, Any?>): Boolean {
|
||||||
|
val containsKey = parameter.name in args
|
||||||
|
// OK to be missing if optional
|
||||||
|
return (parameter.isOptional && !containsKey) || (containsKey && paramCanBeBuilt(args, parameter, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun paramCanBeBuilt(args: Map<String, Any?>, parameter: KParameter, params: HashMap<KParameter, Any?>): Boolean {
|
||||||
|
val value = args[parameter.name]
|
||||||
|
params[parameter] = value
|
||||||
|
return (value is Any && parameterAssignableFrom(parameter.type.javaType, value)) || parameter.type.isMarkedNullable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parameterAssignableFrom(type: Type, value: Any): Boolean {
|
||||||
|
if (type is Class<*>) {
|
||||||
|
if (type.isPrimitive) {
|
||||||
|
return Primitives.unwrap(value.javaClass) == type
|
||||||
|
} else {
|
||||||
|
return type.isAssignableFrom(value.javaClass)
|
||||||
|
}
|
||||||
|
} else if (type is ParameterizedType) {
|
||||||
|
return parameterAssignableFrom(type.rawType, value)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,14 @@
|
|||||||
package net.corda.core.flows;
|
package net.corda.node.services.events;
|
||||||
|
|
||||||
import org.junit.*;
|
import net.corda.core.flows.FlowLogic;
|
||||||
|
import net.corda.core.flows.FlowLogicRefFactory;
|
||||||
|
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class FlowLogicRefFromJavaTest {
|
public class FlowLogicRefFromJavaTest {
|
||||||
|
|
||||||
@ -55,7 +61,7 @@ public class FlowLogicRefFromJavaTest {
|
|||||||
argsList.add(ParamType1.class.getName());
|
argsList.add(ParamType1.class.getName());
|
||||||
argsList.add(ParamType2.class.getName());
|
argsList.add(ParamType2.class.getName());
|
||||||
whiteList.put(JavaFlowLogic.class.getName(), argsList);
|
whiteList.put(JavaFlowLogic.class.getName(), argsList);
|
||||||
FlowLogicRefFactory factory = new FlowLogicRefFactory(whiteList);
|
FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList);
|
||||||
factory.create(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
|
factory.create(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +69,7 @@ public class FlowLogicRefFromJavaTest {
|
|||||||
public void testNoArg() {
|
public void testNoArg() {
|
||||||
Map<String, Set<String>> whiteList = new HashMap<>();
|
Map<String, Set<String>> whiteList = new HashMap<>();
|
||||||
whiteList.put(JavaNoArgFlowLogic.class.getName(), new HashSet<>());
|
whiteList.put(JavaNoArgFlowLogic.class.getName(), new HashSet<>());
|
||||||
FlowLogicRefFactory factory = new FlowLogicRefFactory(whiteList);
|
FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList);
|
||||||
factory.create(JavaNoArgFlowLogic.class);
|
factory.create(JavaNoArgFlowLogic.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,7 +4,6 @@ import com.codahale.metrics.MetricRegistry
|
|||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.FlowInitiator
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
@ -13,6 +12,7 @@ import net.corda.node.serialization.NodeClock
|
|||||||
import net.corda.node.services.api.*
|
import net.corda.node.services.api.*
|
||||||
import net.corda.node.services.messaging.MessagingService
|
import net.corda.node.services.messaging.MessagingService
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
import net.corda.node.services.schema.NodeSchemaService
|
||||||
|
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
import net.corda.node.services.statemachine.StateMachineManager
|
||||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||||
@ -30,7 +30,7 @@ open class MockServiceHubInternal(
|
|||||||
val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(),
|
val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(),
|
||||||
val scheduler: SchedulerService? = null,
|
val scheduler: SchedulerService? = null,
|
||||||
val overrideClock: Clock? = NodeClock(),
|
val overrideClock: Clock? = NodeClock(),
|
||||||
val flowFactory: FlowLogicRefFactory? = FlowLogicRefFactory(),
|
val flowFactory: FlowLogicRefFactoryInternal? = FlowLogicRefFactoryImpl(),
|
||||||
val schemas: SchemaService? = NodeSchemaService(),
|
val schemas: SchemaService? = NodeSchemaService(),
|
||||||
val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2)
|
val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2)
|
||||||
) : ServiceHubInternal() {
|
) : ServiceHubInternal() {
|
||||||
@ -56,7 +56,7 @@ open class MockServiceHubInternal(
|
|||||||
get() = throw UnsupportedOperationException()
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||||
override val flowLogicRefFactory: FlowLogicRefFactory
|
override val flowLogicRefFactory: FlowLogicRefFactoryInternal
|
||||||
get() = flowFactory ?: throw UnsupportedOperationException()
|
get() = flowFactory ?: throw UnsupportedOperationException()
|
||||||
override val schemaService: SchemaService
|
override val schemaService: SchemaService
|
||||||
get() = schemas ?: throw UnsupportedOperationException()
|
get() = schemas ?: throw UnsupportedOperationException()
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.node.services.events
|
||||||
|
|
||||||
import net.corda.core.days
|
import net.corda.core.days
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@ -34,12 +36,12 @@ class FlowLogicRefTest {
|
|||||||
override fun call() = Unit
|
override fun call() = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var factory: FlowLogicRefFactory
|
lateinit var factory: FlowLogicRefFactoryImpl
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
// We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them
|
// We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them
|
||||||
factory = FlowLogicRefFactory(mapOf(Pair(KotlinFlowLogic::class.java.name, setOf(ParamType1::class.java.name, ParamType2::class.java.name)),
|
factory = FlowLogicRefFactoryImpl(mapOf(Pair(KotlinFlowLogic::class.java.name, setOf(ParamType1::class.java.name, ParamType2::class.java.name)),
|
||||||
Pair(KotlinNoArgFlowLogic::class.java.name, setOf())))
|
Pair(KotlinNoArgFlowLogic::class.java.name, setOf())))
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import net.corda.core.utilities.ALICE_KEY
|
|||||||
import net.corda.core.utilities.DUMMY_NOTARY
|
import net.corda.core.utilities.DUMMY_NOTARY
|
||||||
import net.corda.node.services.MockServiceHubInternal
|
import net.corda.node.services.MockServiceHubInternal
|
||||||
import net.corda.node.services.persistence.DBCheckpointStorage
|
import net.corda.node.services.persistence.DBCheckpointStorage
|
||||||
|
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
import net.corda.node.services.statemachine.StateMachineManager
|
||||||
import net.corda.node.services.vault.NodeVaultService
|
import net.corda.node.services.vault.NodeVaultService
|
||||||
import net.corda.node.utilities.AffinityExecutor
|
import net.corda.node.utilities.AffinityExecutor
|
||||||
@ -45,7 +46,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
|||||||
|
|
||||||
// We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them
|
// We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them
|
||||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||||
val factory = FlowLogicRefFactory(mapOf(Pair(TestFlowLogic::class.java.name, setOf(NodeSchedulerServiceTest::class.java.name, Integer::class.java.name))))
|
val factory = FlowLogicRefFactoryImpl(mapOf(Pair(TestFlowLogic::class.java.name, setOf(NodeSchedulerServiceTest::class.java.name, Integer::class.java.name))))
|
||||||
|
|
||||||
lateinit var services: MockServiceHubInternal
|
lateinit var services: MockServiceHubInternal
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user