Skip to content

Commit 0c3e58c

Browse files
committed
feat(corda-connector): params factory pattern support #620
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 <[email protected]>
1 parent 81c1f7f commit 0c3e58c

File tree

11 files changed

+265
-195
lines changed

11 files changed

+265
-195
lines changed

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/impl/ApiPluginLedgerConnectorCordaServiceImpl.kt

+5-116
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,10 @@ import net.schmizz.sshj.userauth.password.PasswordUtils
2121
import net.schmizz.sshj.xfer.InMemorySourceFile
2222
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.api.ApiPluginLedgerConnectorCordaService
2323
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.*
24-
import org.xeustechnologies.jcl.JarClassLoader
2524
import java.io.IOException
2625
import java.io.InputStream
2726
import java.lang.Exception
28-
import java.lang.IllegalStateException
2927
import java.lang.RuntimeException
30-
import java.lang.reflect.Constructor
3128
import java.util.*
3229
import java.util.concurrent.TimeUnit
3330
import kotlin.IllegalArgumentException
@@ -44,6 +41,7 @@ class ApiPluginLedgerConnectorCordaServiceImpl(
4441
) : ApiPluginLedgerConnectorCordaService {
4542

4643
companion object {
44+
val logger = loggerFor<ApiPluginLedgerConnectorCordaServiceImpl>()
4745

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

5452
val writer: ObjectWriter = mapper.writer()
5553

56-
val jcl: JarClassLoader = JarClassLoader(ApiPluginLedgerConnectorCordaServiceImpl::class.java.classLoader)
57-
58-
val logger = loggerFor<ApiPluginLedgerConnectorCordaServiceImpl>()
59-
60-
// If something is missing from here that's because they also missed at in the documentation:
61-
// https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
62-
val exoticTypes: Map<String, Class<*>> = mapOf(
63-
64-
"byte" to Byte::class.java,
65-
"char" to Char::class.java,
66-
"int" to Int::class.java,
67-
"short" to Short::class.java,
68-
"long" to Long::class.java,
69-
"float" to Float::class.java,
70-
"double" to Double::class.java,
71-
"boolean" to Boolean::class.java,
72-
73-
"byte[]" to ByteArray::class.java,
74-
"char[]" to CharArray::class.java,
75-
"int[]" to IntArray::class.java,
76-
"short[]" to ShortArray::class.java,
77-
"long[]" to LongArray::class.java,
78-
"float[]" to FloatArray::class.java,
79-
"double[]" to DoubleArray::class.java,
80-
"boolean[]" to BooleanArray::class.java
81-
)
82-
}
83-
84-
fun getOrInferType(fqClassName: String): Class<*> {
85-
Objects.requireNonNull(fqClassName, "fqClassName must not be null for its type to be inferred.")
86-
87-
return if (exoticTypes.containsKey(fqClassName)) {
88-
exoticTypes.getOrElse(
89-
fqClassName,
90-
{ throw IllegalStateException("Could not locate Class<*> for $fqClassName Exotic JVM types map must have been modified on a concurrent threat.") })
91-
} else {
92-
try {
93-
jcl.loadClass(fqClassName, true)
94-
} catch (ex: ClassNotFoundException) {
95-
Class.forName(fqClassName)
96-
}
97-
}
98-
}
99-
100-
fun instantiate(jvmObject: JvmObject): Any? {
101-
logger.info("Instantiating ... JvmObject={}", jvmObject)
102-
103-
val clazz = getOrInferType(jvmObject.jvmType.fqClassName)
104-
105-
when (jvmObject.jvmTypeKind) {
106-
JvmTypeKind.REFERENCE -> {
107-
if (jvmObject.jvmCtorArgs == null) {
108-
throw IllegalArgumentException("jvmObject.jvmCtorArgs cannot be null when jvmObject.jvmTypeKind == JvmTypeKind.REFERENCE")
109-
}
110-
val constructorArgs: Array<Any?> = jvmObject.jvmCtorArgs.map { x -> instantiate(x) }.toTypedArray()
111-
112-
when {
113-
List::class.java.isAssignableFrom(clazz) -> {
114-
return listOf(*constructorArgs)
115-
}
116-
Currency::class.java.isAssignableFrom(clazz) -> {
117-
// FIXME introduce a more dynamic/flexible way of handling classes with no public constructors....
118-
return Currency.getInstance(jvmObject.jvmCtorArgs.first().primitiveValue as String)
119-
}
120-
Array<Any>::class.java.isAssignableFrom(clazz) -> {
121-
// TODO verify that this actually works and also
122-
// if we need it at all since we already have lists covered
123-
return arrayOf(*constructorArgs)
124-
}
125-
else -> {
126-
val constructorArgTypes: List<Class<*>> =
127-
jvmObject.jvmCtorArgs.map { x -> getOrInferType(x.jvmType.fqClassName) }
128-
val constructor: Constructor<*>
129-
try {
130-
constructor = clazz.constructors
131-
.filter { c -> c.parameterCount == constructorArgTypes.size }
132-
.single { c ->
133-
c.parameterTypes
134-
.mapIndexed { index, clazz -> clazz.isAssignableFrom(constructorArgTypes[index]) }
135-
.all { x -> x }
136-
}
137-
} catch (ex: NoSuchElementException) {
138-
val argTypes = jvmObject.jvmCtorArgs.joinToString(",") { x -> x.jvmType.fqClassName }
139-
val className = jvmObject.jvmType.fqClassName
140-
val constructorsAsStrings = clazz.constructors
141-
.mapIndexed { i, c -> "$className->Constructor#${i + 1}(${c.parameterTypes.joinToString { p -> p.name }})" }
142-
.joinToString(" ;; ")
143-
val targetConstructor = "Cannot find matching constructor for ${className}(${argTypes})"
144-
val availableConstructors =
145-
"Searched among the ${clazz.constructors.size} available constructors: $constructorsAsStrings"
146-
throw RuntimeException("$targetConstructor --- $availableConstructors")
147-
}
148-
149-
logger.info("Constructor=${constructor}")
150-
constructorArgs.forEachIndexed { index, it -> logger.info("Constructor ARGS: #${index} -> $it") }
151-
val instance = constructor.newInstance(*constructorArgs)
152-
logger.info("Instantiated REFERENCE OK {}", instance)
153-
return instance
154-
}
155-
}
156-
157-
}
158-
JvmTypeKind.PRIMITIVE -> {
159-
logger.info("Instantiated PRIMITIVE OK {}", jvmObject.primitiveValue)
160-
return jvmObject.primitiveValue
161-
}
162-
else -> {
163-
throw IllegalArgumentException("Unknown jvmObject.jvmTypeKind (${jvmObject.jvmTypeKind})")
164-
}
165-
}
54+
val jsonJvmObjectDeserializer = JsonJvmObjectDeserializer()
16655
}
16756

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

17463
val flowHandle = when (req.flowInvocationType) {
@@ -365,7 +254,7 @@ class ApiPluginLedgerConnectorCordaServiceImpl(
365254
}
366255
val deployedJarFileNames = deployContractJarsV1Request.jarFiles.map {
367256
val jarFileInputStream = decoder.decode(it.contentBase64).inputStream()
368-
jcl.add(jarFileInputStream)
257+
jsonJvmObjectDeserializer.jcl.add(jarFileInputStream)
369258
logger.info("Added jar to classpath of Corda Connector Plugin Server: ${it.filename}")
370259
it.filename
371260
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package org.hyperledger.cactus.plugin.ledger.connector.corda.server.impl
2+
3+
import net.corda.core.utilities.loggerFor
4+
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.JvmObject
5+
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.JvmTypeKind
6+
import org.xeustechnologies.jcl.JarClassLoader
7+
import java.lang.Exception
8+
import java.lang.IllegalStateException
9+
import java.lang.RuntimeException
10+
import java.lang.reflect.Constructor
11+
import java.lang.reflect.Method
12+
import java.util.*
13+
14+
// FIXME: Make it so that this has a memory, remembering the .jar files that were added before (file-system?) or
15+
// maybe use the keychain to save it there and then it can pre-populate at boot?
16+
class JsonJvmObjectDeserializer(
17+
val jcl: JarClassLoader = JarClassLoader(JsonJvmObjectDeserializer::class.java.classLoader)
18+
) {
19+
20+
companion object {
21+
val logger = loggerFor<JsonJvmObjectDeserializer>()
22+
23+
// If something is missing from here that's because they also missed at in the documentation:
24+
// https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
25+
val exoticTypes: Map<String, Class<*>> = mapOf(
26+
27+
"byte" to Byte::class.java,
28+
"char" to Char::class.java,
29+
"int" to Int::class.java,
30+
"short" to Short::class.java,
31+
"long" to Long::class.java,
32+
"float" to Float::class.java,
33+
"double" to Double::class.java,
34+
"boolean" to Boolean::class.java,
35+
36+
"byte[]" to ByteArray::class.java,
37+
"char[]" to CharArray::class.java,
38+
"int[]" to IntArray::class.java,
39+
"short[]" to ShortArray::class.java,
40+
"long[]" to LongArray::class.java,
41+
"float[]" to FloatArray::class.java,
42+
"double[]" to DoubleArray::class.java,
43+
"boolean[]" to BooleanArray::class.java
44+
)
45+
}
46+
47+
fun getOrInferType(fqClassName: String): Class<*> {
48+
Objects.requireNonNull(fqClassName, "fqClassName must not be null for its type to be inferred.")
49+
50+
return if (exoticTypes.containsKey(fqClassName)) {
51+
exoticTypes.getOrElse(
52+
fqClassName,
53+
{ throw IllegalStateException("Could not locate Class<*> for $fqClassName Exotic JVM types map must have been modified on a concurrent threat.") })
54+
} else {
55+
try {
56+
jcl.loadClass(fqClassName, true)
57+
} catch (ex: ClassNotFoundException) {
58+
Class.forName(fqClassName)
59+
}
60+
}
61+
}
62+
63+
fun instantiate(jvmObject: JvmObject): Any? {
64+
logger.info("Instantiating ... JvmObject={}", jvmObject)
65+
66+
val clazz = getOrInferType(jvmObject.jvmType.fqClassName)
67+
68+
when (jvmObject.jvmTypeKind) {
69+
JvmTypeKind.REFERENCE -> {
70+
if (jvmObject.jvmCtorArgs == null) {
71+
throw IllegalArgumentException("jvmObject.jvmCtorArgs cannot be null when jvmObject.jvmTypeKind == JvmTypeKind.REFERENCE")
72+
}
73+
val constructorArgs: Array<Any?> = jvmObject.jvmCtorArgs.map { x -> instantiate(x) }.toTypedArray()
74+
75+
when {
76+
List::class.java.isAssignableFrom(clazz) -> {
77+
return listOf(*constructorArgs)
78+
}
79+
Array<Any>::class.java.isAssignableFrom(clazz) -> {
80+
// TODO verify that this actually works and also
81+
// if we need it at all since we already have lists covered
82+
return arrayOf(*constructorArgs)
83+
}
84+
jvmObject.jvmType.constructorName != null -> {
85+
val methodArgTypes: List<Class<*>> =
86+
jvmObject.jvmCtorArgs.map { x -> getOrInferType(x.jvmType.fqClassName) }
87+
val factoryMethod: Method
88+
try {
89+
factoryMethod = clazz.methods
90+
.filter { c -> c.name == jvmObject.jvmType.constructorName }
91+
.filter { c -> c.parameterCount == methodArgTypes.size }
92+
.single { c ->
93+
c.parameterTypes
94+
.mapIndexed { index, clazz -> clazz.isAssignableFrom(methodArgTypes[index]) }
95+
.all { x -> x }
96+
}
97+
} catch (ex: NoSuchElementException) {
98+
val argTypes = jvmObject.jvmCtorArgs.joinToString(",") { x -> x.jvmType.fqClassName }
99+
val className = jvmObject.jvmType.fqClassName
100+
val methodsAsStrings = clazz.constructors
101+
.mapIndexed { i, c -> "$className->Method#${i + 1}(${c.parameterTypes.joinToString { p -> p.name }})" }
102+
.joinToString(" ;; ")
103+
val targetMethod = "Cannot find matching method for ${className}(${argTypes})"
104+
val availableMethods =
105+
"Searched among the ${clazz.constructors.size} available methods: $methodsAsStrings"
106+
throw RuntimeException("$targetMethod --- $availableMethods")
107+
}
108+
109+
logger.info("Constructor=${factoryMethod}")
110+
constructorArgs.forEachIndexed { index, it -> logger.info("Constructor ARGS: #${index} -> $it") }
111+
112+
var invocationTarget: Any? = null
113+
if (jvmObject.jvmType.invocationTarget != null) {
114+
try {
115+
logger.debug("Instantiating InvocationTarget: ${jvmObject.jvmType.invocationTarget}")
116+
invocationTarget = instantiate(jvmObject.jvmType.invocationTarget)
117+
logger.debug("Instantiated OK InvocationTarget: ${jvmObject.jvmType.invocationTarget}")
118+
} catch (ex: Exception) {
119+
val argTypes = jvmObject.jvmCtorArgs.joinToString(",") { x -> x.jvmType.fqClassName }
120+
val className = jvmObject.jvmType.fqClassName
121+
val constructorName = jvmObject.jvmType.constructorName
122+
val message = "Failed to instantiate invocation target for " +
123+
"JvmType:${className}${constructorName}(${argTypes}) with an " +
124+
"InvocationTarget: ${jvmObject.jvmType.invocationTarget}"
125+
throw RuntimeException(message, ex)
126+
}
127+
}
128+
val instance = factoryMethod.invoke(invocationTarget, *constructorArgs)
129+
logger.info("Instantiated REFERENCE OK {}", instance)
130+
return instance
131+
}
132+
else -> {
133+
val constructorArgTypes: List<Class<*>> =
134+
jvmObject.jvmCtorArgs.map { x -> getOrInferType(x.jvmType.fqClassName) }
135+
val constructor: Constructor<*>
136+
try {
137+
constructor = clazz.constructors
138+
.filter { c -> c.parameterCount == constructorArgTypes.size }
139+
.single { c ->
140+
c.parameterTypes
141+
.mapIndexed { index, clazz -> clazz.isAssignableFrom(constructorArgTypes[index]) }
142+
.all { x -> x }
143+
}
144+
} catch (ex: NoSuchElementException) {
145+
val argTypes = jvmObject.jvmCtorArgs.joinToString(",") { x -> x.jvmType.fqClassName }
146+
val className = jvmObject.jvmType.fqClassName
147+
val constructorsAsStrings = clazz.constructors
148+
.mapIndexed { i, c -> "$className->Constructor#${i + 1}(${c.parameterTypes.joinToString { p -> p.name }})" }
149+
.joinToString(" ;; ")
150+
val targetConstructor = "Cannot find matching constructor for ${className}(${argTypes})"
151+
val availableConstructors =
152+
"Searched among the ${clazz.constructors.size} available constructors: $constructorsAsStrings"
153+
throw RuntimeException("$targetConstructor --- $availableConstructors")
154+
}
155+
156+
logger.info("Constructor=${constructor}")
157+
constructorArgs.forEachIndexed { index, it -> logger.info("Constructor ARGS: #${index} -> $it") }
158+
val instance = constructor.newInstance(*constructorArgs)
159+
logger.info("Instantiated REFERENCE OK {}", instance)
160+
return instance
161+
}
162+
}
163+
164+
}
165+
JvmTypeKind.PRIMITIVE -> {
166+
logger.info("Instantiated PRIMITIVE OK {}", jvmObject.primitiveValue)
167+
return jvmObject.primitiveValue
168+
}
169+
else -> {
170+
throw IllegalArgumentException("Unknown jvmObject.jvmTypeKind (${jvmObject.jvmTypeKind})")
171+
}
172+
}
173+
}
174+
}

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/impl/NodeRPCConnection.kt

+18-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,24 @@ open class NodeRPCConnection(
6969
maxAttempts = 30
7070
)
7171

72-
rpcConnection = rpcClient.start(username, password, gracefulReconnect = gracefulReconnect)
72+
// this workaround here is due to the Graceful Reconnect above not actually doing what it's supposed to
73+
// either because it has a bug or because I misread the documentation.
74+
// So this manual retry on top of the graceful reconnects is to make it resilient
75+
var numberOfTriesRemaining = 5
76+
while (numberOfTriesRemaining > 0) {
77+
numberOfTriesRemaining--
78+
try {
79+
logger.info("Trying to connect to RPC numberOfTriesRemaining=$numberOfTriesRemaining")
80+
rpcConnection = rpcClient.start(username, password, gracefulReconnect = gracefulReconnect)
81+
break;
82+
} catch (ex: net.corda.client.rpc.RPCException) {
83+
logger.info("ManualReconnect:numberOfTriesRemaining=$numberOfTriesRemaining")
84+
if (numberOfTriesRemaining <= 0) {
85+
throw ex
86+
}
87+
}
88+
}
89+
7390
proxy = rpcConnection.proxy
7491
}
7592

packages/cactus-plugin-ledger-connector-corda/src/main-server/kotlin/gen/kotlin-spring/src/main/kotlin/org/hyperledger/cactus/plugin/ledger/connector/corda/server/model/DeployContractJarsV1Request.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ data class DeployContractJarsV1Request(
2222

2323
@get:NotNull
2424
@field:Valid
25-
@get:Size(min=1,max=1024)
25+
@get:Size(min=0,max=1024)
2626
@field:JsonProperty("cordappDeploymentConfigs") val cordappDeploymentConfigs: kotlin.collections.List<CordappDeploymentConfig>,
2727

2828
@get:NotNull

0 commit comments

Comments
 (0)