Skip to content

Commit f3d1856

Browse files
committed
feat(corda-connector): params factory pattern support hyperledger-cacti#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 hyperledger-cacti#620 Signed-off-by: Peter Somogyvari <[email protected]>
1 parent c966769 commit f3d1856

File tree

8 files changed

+227
-169
lines changed

8 files changed

+227
-169
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/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

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/JvmType.kt

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.hyperledger.cactus.plugin.ledger.connector.corda.server.model
22

33
import java.util.Objects
44
import com.fasterxml.jackson.annotation.JsonProperty
5+
import org.hyperledger.cactus.plugin.ledger.connector.corda.server.model.JvmObject
56
import javax.validation.constraints.DecimalMax
67
import javax.validation.constraints.DecimalMin
78
import javax.validation.constraints.Max
@@ -14,12 +15,20 @@ import javax.validation.Valid
1415
/**
1516
* Represents a reference to a JVM type (such as a Java class)
1617
* @param fqClassName
18+
* @param constructorName This parameter is used to specify that the function used to construct this JvmType is not a constructor function but instead is a factory function. Setting this parameter will cause the plugin to look up methods of the class denoted by fqClassName instead of its constructors.
19+
* @param invocationTarget
1720
*/
1821
data class JvmType(
1922

2023
@get:NotNull
2124
@get:Size(min=1,max=65535)
22-
@field:JsonProperty("fqClassName") val fqClassName: kotlin.String
25+
@field:JsonProperty("fqClassName") val fqClassName: kotlin.String,
26+
27+
@get:Size(min=1,max=65535)
28+
@field:JsonProperty("constructorName") val constructorName: kotlin.String? = null,
29+
30+
@field:Valid
31+
@field:JsonProperty("invocationTarget") val invocationTarget: JvmObject? = null
2332
) {
2433

2534
}

0 commit comments

Comments
 (0)