Skip to content

Commit

Permalink
feat(corda-connector): params factory pattern support #620
Browse files Browse the repository at this point in the history
Primary change
============

Added support in the Corda ledger connector plugin's JVM backend to
have the JSON DSL be able to express static and non-static factory
functions.
For the non-static factory functions, the invocation target can be any
constructable object that the DLS can express via the `JvmObject` type.

The method lookups are performed the same way as when looking up
constructors but with the additional constraint that the name of the
method has to also match not just the parameter types/count.

Miscellaneous changes
==================

Refactored ApiPluginLedgerConnectorCordaServiceImpl.kt so that it
does not include the JSON DSL deserialization logic within itself but
instead outsources all of that to a separate class that was newly added
just for this: JsonJvmObjectDeserializer.kt

Updated the tests to specify the new invocation parameters accordingly:
The Currency class is now instantiated through the JSON DLS thanks to
the static factory method support we just added.

Published the container image to the DockerHub registry with the updated
JVM corda connector plugin under the tag:
hyperledger/cactus-connector-corda-server:2021-03-24-feat-620
(which is now used by both of the integration tests that we
currently have for corda)

The contract deployment request object will now allow a minimum of
zero items in the deployment configuration array parameter which
we needed to cover the case when a jar only needs to be deployed
to the classpath of the connector plugin because it is already present
on the Corda node's cordapps directory (meaning that adding it there
again would make it impossible to start back up the corda node)

Fixes #620

Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz committed Mar 28, 2021
1 parent 81c1f7f commit 0c3e58c
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 195 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,10 @@ import net.schmizz.sshj.userauth.password.PasswordUtils
import net.schmizz.sshj.xfer.InMemorySourceFile
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.api.ApiPluginLedgerConnectorCordaService
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.*
import org.xeustechnologies.jcl.JarClassLoader
import java.io.IOException
import java.io.InputStream
import java.lang.Exception
import java.lang.IllegalStateException
import java.lang.RuntimeException
import java.lang.reflect.Constructor
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.IllegalArgumentException
Expand All @@ -44,6 +41,7 @@ class ApiPluginLedgerConnectorCordaServiceImpl(
) : ApiPluginLedgerConnectorCordaService {

companion object {
val logger = loggerFor<ApiPluginLedgerConnectorCordaServiceImpl>()

// FIXME: do not recreate the mapper for every service implementation instance that we create...
val mapper: ObjectMapper = jacksonObjectMapper()
Expand All @@ -53,122 +51,13 @@ class ApiPluginLedgerConnectorCordaServiceImpl(

val writer: ObjectWriter = mapper.writer()

val jcl: JarClassLoader = JarClassLoader(ApiPluginLedgerConnectorCordaServiceImpl::class.java.classLoader)

val logger = loggerFor<ApiPluginLedgerConnectorCordaServiceImpl>()

// If something is missing from here that's because they also missed at in the documentation:
// https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
val exoticTypes: Map<String, Class<*>> = mapOf(

"byte" to Byte::class.java,
"char" to Char::class.java,
"int" to Int::class.java,
"short" to Short::class.java,
"long" to Long::class.java,
"float" to Float::class.java,
"double" to Double::class.java,
"boolean" to Boolean::class.java,

"byte[]" to ByteArray::class.java,
"char[]" to CharArray::class.java,
"int[]" to IntArray::class.java,
"short[]" to ShortArray::class.java,
"long[]" to LongArray::class.java,
"float[]" to FloatArray::class.java,
"double[]" to DoubleArray::class.java,
"boolean[]" to BooleanArray::class.java
)
}

fun getOrInferType(fqClassName: String): Class<*> {
Objects.requireNonNull(fqClassName, "fqClassName must not be null for its type to be inferred.")

return if (exoticTypes.containsKey(fqClassName)) {
exoticTypes.getOrElse(
fqClassName,
{ throw IllegalStateException("Could not locate Class<*> for $fqClassName Exotic JVM types map must have been modified on a concurrent threat.") })
} else {
try {
jcl.loadClass(fqClassName, true)
} catch (ex: ClassNotFoundException) {
Class.forName(fqClassName)
}
}
}

fun instantiate(jvmObject: JvmObject): Any? {
logger.info("Instantiating ... JvmObject={}", jvmObject)

val clazz = getOrInferType(jvmObject.jvmType.fqClassName)

when (jvmObject.jvmTypeKind) {
JvmTypeKind.REFERENCE -> {
if (jvmObject.jvmCtorArgs == null) {
throw IllegalArgumentException("jvmObject.jvmCtorArgs cannot be null when jvmObject.jvmTypeKind == JvmTypeKind.REFERENCE")
}
val constructorArgs: Array<Any?> = jvmObject.jvmCtorArgs.map { x -> instantiate(x) }.toTypedArray()

when {
List::class.java.isAssignableFrom(clazz) -> {
return listOf(*constructorArgs)
}
Currency::class.java.isAssignableFrom(clazz) -> {
// FIXME introduce a more dynamic/flexible way of handling classes with no public constructors....
return Currency.getInstance(jvmObject.jvmCtorArgs.first().primitiveValue as String)
}
Array<Any>::class.java.isAssignableFrom(clazz) -> {
// TODO verify that this actually works and also
// if we need it at all since we already have lists covered
return arrayOf(*constructorArgs)
}
else -> {
val constructorArgTypes: List<Class<*>> =
jvmObject.jvmCtorArgs.map { x -> getOrInferType(x.jvmType.fqClassName) }
val constructor: Constructor<*>
try {
constructor = clazz.constructors
.filter { c -> c.parameterCount == constructorArgTypes.size }
.single { c ->
c.parameterTypes
.mapIndexed { index, clazz -> clazz.isAssignableFrom(constructorArgTypes[index]) }
.all { x -> x }
}
} catch (ex: NoSuchElementException) {
val argTypes = jvmObject.jvmCtorArgs.joinToString(",") { x -> x.jvmType.fqClassName }
val className = jvmObject.jvmType.fqClassName
val constructorsAsStrings = clazz.constructors
.mapIndexed { i, c -> "$className->Constructor#${i + 1}(${c.parameterTypes.joinToString { p -> p.name }})" }
.joinToString(" ;; ")
val targetConstructor = "Cannot find matching constructor for ${className}(${argTypes})"
val availableConstructors =
"Searched among the ${clazz.constructors.size} available constructors: $constructorsAsStrings"
throw RuntimeException("$targetConstructor --- $availableConstructors")
}

logger.info("Constructor=${constructor}")
constructorArgs.forEachIndexed { index, it -> logger.info("Constructor ARGS: #${index} -> $it") }
val instance = constructor.newInstance(*constructorArgs)
logger.info("Instantiated REFERENCE OK {}", instance)
return instance
}
}

}
JvmTypeKind.PRIMITIVE -> {
logger.info("Instantiated PRIMITIVE OK {}", jvmObject.primitiveValue)
return jvmObject.primitiveValue
}
else -> {
throw IllegalArgumentException("Unknown jvmObject.jvmTypeKind (${jvmObject.jvmTypeKind})")
}
}
val jsonJvmObjectDeserializer = JsonJvmObjectDeserializer()
}

fun dynamicInvoke(rpc: CordaRPCOps, req: InvokeContractV1Request): InvokeContractV1Response {
@Suppress("UNCHECKED_CAST")
val classFlowLogic = getOrInferType(req.flowFullClassName) as Class<out FlowLogic<*>>
val params = req.params.map { p -> instantiate(p) }.toTypedArray()
val classFlowLogic = jsonJvmObjectDeserializer.getOrInferType(req.flowFullClassName) as Class<out FlowLogic<*>>
val params = req.params.map { p -> jsonJvmObjectDeserializer.instantiate(p) }.toTypedArray()
logger.info("params={}", params)

val flowHandle = when (req.flowInvocationType) {
Expand Down Expand Up @@ -365,7 +254,7 @@ class ApiPluginLedgerConnectorCordaServiceImpl(
}
val deployedJarFileNames = deployContractJarsV1Request.jarFiles.map {
val jarFileInputStream = decoder.decode(it.contentBase64).inputStream()
jcl.add(jarFileInputStream)
jsonJvmObjectDeserializer.jcl.add(jarFileInputStream)
logger.info("Added jar to classpath of Corda Connector Plugin Server: ${it.filename}")
it.filename
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package org.hyperledger.cactus.plugin.ledger.connector.corda.server.impl

import net.corda.core.utilities.loggerFor
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.JvmObject
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.JvmTypeKind
import org.xeustechnologies.jcl.JarClassLoader
import java.lang.Exception
import java.lang.IllegalStateException
import java.lang.RuntimeException
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*

// FIXME: Make it so that this has a memory, remembering the .jar files that were added before (file-system?) or
// maybe use the keychain to save it there and then it can pre-populate at boot?
class JsonJvmObjectDeserializer(
val jcl: JarClassLoader = JarClassLoader(JsonJvmObjectDeserializer::class.java.classLoader)
) {

companion object {
val logger = loggerFor<JsonJvmObjectDeserializer>()

// If something is missing from here that's because they also missed at in the documentation:
// https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
val exoticTypes: Map<String, Class<*>> = mapOf(

"byte" to Byte::class.java,
"char" to Char::class.java,
"int" to Int::class.java,
"short" to Short::class.java,
"long" to Long::class.java,
"float" to Float::class.java,
"double" to Double::class.java,
"boolean" to Boolean::class.java,

"byte[]" to ByteArray::class.java,
"char[]" to CharArray::class.java,
"int[]" to IntArray::class.java,
"short[]" to ShortArray::class.java,
"long[]" to LongArray::class.java,
"float[]" to FloatArray::class.java,
"double[]" to DoubleArray::class.java,
"boolean[]" to BooleanArray::class.java
)
}

fun getOrInferType(fqClassName: String): Class<*> {
Objects.requireNonNull(fqClassName, "fqClassName must not be null for its type to be inferred.")

return if (exoticTypes.containsKey(fqClassName)) {
exoticTypes.getOrElse(
fqClassName,
{ throw IllegalStateException("Could not locate Class<*> for $fqClassName Exotic JVM types map must have been modified on a concurrent threat.") })
} else {
try {
jcl.loadClass(fqClassName, true)
} catch (ex: ClassNotFoundException) {
Class.forName(fqClassName)
}
}
}

fun instantiate(jvmObject: JvmObject): Any? {
logger.info("Instantiating ... JvmObject={}", jvmObject)

val clazz = getOrInferType(jvmObject.jvmType.fqClassName)

when (jvmObject.jvmTypeKind) {
JvmTypeKind.REFERENCE -> {
if (jvmObject.jvmCtorArgs == null) {
throw IllegalArgumentException("jvmObject.jvmCtorArgs cannot be null when jvmObject.jvmTypeKind == JvmTypeKind.REFERENCE")
}
val constructorArgs: Array<Any?> = jvmObject.jvmCtorArgs.map { x -> instantiate(x) }.toTypedArray()

when {
List::class.java.isAssignableFrom(clazz) -> {
return listOf(*constructorArgs)
}
Array<Any>::class.java.isAssignableFrom(clazz) -> {
// TODO verify that this actually works and also
// if we need it at all since we already have lists covered
return arrayOf(*constructorArgs)
}
jvmObject.jvmType.constructorName != null -> {
val methodArgTypes: List<Class<*>> =
jvmObject.jvmCtorArgs.map { x -> getOrInferType(x.jvmType.fqClassName) }
val factoryMethod: Method
try {
factoryMethod = clazz.methods
.filter { c -> c.name == jvmObject.jvmType.constructorName }
.filter { c -> c.parameterCount == methodArgTypes.size }
.single { c ->
c.parameterTypes
.mapIndexed { index, clazz -> clazz.isAssignableFrom(methodArgTypes[index]) }
.all { x -> x }
}
} catch (ex: NoSuchElementException) {
val argTypes = jvmObject.jvmCtorArgs.joinToString(",") { x -> x.jvmType.fqClassName }
val className = jvmObject.jvmType.fqClassName
val methodsAsStrings = clazz.constructors
.mapIndexed { i, c -> "$className->Method#${i + 1}(${c.parameterTypes.joinToString { p -> p.name }})" }
.joinToString(" ;; ")
val targetMethod = "Cannot find matching method for ${className}(${argTypes})"
val availableMethods =
"Searched among the ${clazz.constructors.size} available methods: $methodsAsStrings"
throw RuntimeException("$targetMethod --- $availableMethods")
}

logger.info("Constructor=${factoryMethod}")
constructorArgs.forEachIndexed { index, it -> logger.info("Constructor ARGS: #${index} -> $it") }

var invocationTarget: Any? = null
if (jvmObject.jvmType.invocationTarget != null) {
try {
logger.debug("Instantiating InvocationTarget: ${jvmObject.jvmType.invocationTarget}")
invocationTarget = instantiate(jvmObject.jvmType.invocationTarget)
logger.debug("Instantiated OK InvocationTarget: ${jvmObject.jvmType.invocationTarget}")
} catch (ex: Exception) {
val argTypes = jvmObject.jvmCtorArgs.joinToString(",") { x -> x.jvmType.fqClassName }
val className = jvmObject.jvmType.fqClassName
val constructorName = jvmObject.jvmType.constructorName
val message = "Failed to instantiate invocation target for " +
"JvmType:${className}${constructorName}(${argTypes}) with an " +
"InvocationTarget: ${jvmObject.jvmType.invocationTarget}"
throw RuntimeException(message, ex)
}
}
val instance = factoryMethod.invoke(invocationTarget, *constructorArgs)
logger.info("Instantiated REFERENCE OK {}", instance)
return instance
}
else -> {
val constructorArgTypes: List<Class<*>> =
jvmObject.jvmCtorArgs.map { x -> getOrInferType(x.jvmType.fqClassName) }
val constructor: Constructor<*>
try {
constructor = clazz.constructors
.filter { c -> c.parameterCount == constructorArgTypes.size }
.single { c ->
c.parameterTypes
.mapIndexed { index, clazz -> clazz.isAssignableFrom(constructorArgTypes[index]) }
.all { x -> x }
}
} catch (ex: NoSuchElementException) {
val argTypes = jvmObject.jvmCtorArgs.joinToString(",") { x -> x.jvmType.fqClassName }
val className = jvmObject.jvmType.fqClassName
val constructorsAsStrings = clazz.constructors
.mapIndexed { i, c -> "$className->Constructor#${i + 1}(${c.parameterTypes.joinToString { p -> p.name }})" }
.joinToString(" ;; ")
val targetConstructor = "Cannot find matching constructor for ${className}(${argTypes})"
val availableConstructors =
"Searched among the ${clazz.constructors.size} available constructors: $constructorsAsStrings"
throw RuntimeException("$targetConstructor --- $availableConstructors")
}

logger.info("Constructor=${constructor}")
constructorArgs.forEachIndexed { index, it -> logger.info("Constructor ARGS: #${index} -> $it") }
val instance = constructor.newInstance(*constructorArgs)
logger.info("Instantiated REFERENCE OK {}", instance)
return instance
}
}

}
JvmTypeKind.PRIMITIVE -> {
logger.info("Instantiated PRIMITIVE OK {}", jvmObject.primitiveValue)
return jvmObject.primitiveValue
}
else -> {
throw IllegalArgumentException("Unknown jvmObject.jvmTypeKind (${jvmObject.jvmTypeKind})")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,24 @@ open class NodeRPCConnection(
maxAttempts = 30
)

rpcConnection = rpcClient.start(username, password, gracefulReconnect = gracefulReconnect)
// this workaround here is due to the Graceful Reconnect above not actually doing what it's supposed to
// either because it has a bug or because I misread the documentation.
// So this manual retry on top of the graceful reconnects is to make it resilient
var numberOfTriesRemaining = 5
while (numberOfTriesRemaining > 0) {
numberOfTriesRemaining--
try {
logger.info("Trying to connect to RPC numberOfTriesRemaining=$numberOfTriesRemaining")
rpcConnection = rpcClient.start(username, password, gracefulReconnect = gracefulReconnect)
break;
} catch (ex: net.corda.client.rpc.RPCException) {
logger.info("ManualReconnect:numberOfTriesRemaining=$numberOfTriesRemaining")
if (numberOfTriesRemaining <= 0) {
throw ex
}
}
}

proxy = rpcConnection.proxy
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ data class DeployContractJarsV1Request(

@get:NotNull
@field:Valid
@get:Size(min=1,max=1024)
@get:Size(min=0,max=1024)
@field:JsonProperty("cordappDeploymentConfigs") val cordappDeploymentConfigs: kotlin.collections.List<CordappDeploymentConfig>,

@get:NotNull
Expand Down

0 comments on commit 0c3e58c

Please sign in to comment.