CORDA-2773: Support for custom Jackson serializers (#5025)

This commit is contained in:
Tudor Malene
2019-04-26 15:34:42 +01:00
committed by Shams Asari
parent 7b0d177a34
commit f7bf1b41e0
6 changed files with 122 additions and 23 deletions

View File

@ -239,8 +239,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
/** Returns a string-to-string map of commands to a string describing available parameter types. */ /** Returns a string-to-string map of commands to a string describing available parameter types. */
val availableCommands: Map<String, String> val availableCommands: Map<String, String>
get() { get() {
return methodMap.entries().map { entry -> return methodMap.entries().map { (name, args) ->
val (name, args) = entry // TODO: Kotlin 1.1
val argStr = if (args.parameterCount == 0) "" else { val argStr = if (args.parameterCount == 0) "" else {
val paramNames = methodParamNames[name]!! val paramNames = methodParamNames[name]!!
val typeNames = args.parameters.map { it.type.simpleName } val typeNames = args.parameters.map { it.type.simpleName }

View File

@ -0,0 +1,15 @@
package net.corda.client.jackson.internal
import com.fasterxml.jackson.databind.Module
/**
* Should be implemented by CorDapps who wish to declare custom serializers.
* Classes of this type will be autodiscovered and registered.
*/
interface CustomShellSerializationFactory {
/**
* The returned [Module] will be registered automatically with the interactive shell.
*/
fun createJacksonModule(): Module
}

View File

@ -1,17 +1,23 @@
package net.corda.tools.shell package net.corda.tools.shell
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.google.common.io.Files import com.google.common.io.Files
import com.jcraft.jsch.ChannelExec import com.jcraft.jsch.ChannelExec
import com.jcraft.jsch.JSch import com.jcraft.jsch.JSch
import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doAnswer import com.nhaarman.mockito_kotlin.doAnswer
import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.mock
import net.corda.client.jackson.internal.CustomShellSerializationFactory
import net.corda.client.rpc.RPCException import net.corda.client.rpc.RPCException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions import net.corda.node.services.Permissions
@ -27,6 +33,8 @@ import net.corda.testing.driver.driver
import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.driver.internal.NodeHandleInternal
import net.corda.testing.internal.useSslRpcOverrides import net.corda.testing.internal.useSslRpcOverrides
import net.corda.testing.node.User import net.corda.testing.node.User
import net.corda.tools.shell.InteractiveShell.createYamlInputMapper
import net.corda.tools.shell.InteractiveShell.runFlowByNameFragment
import net.corda.tools.shell.utlities.ANSIProgressRenderer import net.corda.tools.shell.utlities.ANSIProgressRenderer
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -268,11 +276,11 @@ class InteractiveShellIntegrationTest {
} }
} }
val ansiProgressRenderer = mock<ANSIProgressRenderer> { val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
} }
InteractiveShell.runFlowByNameFragment( runFlowByNameFragment(
"NoOpFlow", "NoOpFlow",
"", output, node.rpc, ansiProgressRenderer) "", output, node.rpc, ansiProgressRenderer, createYamlInputMapper(node.rpc, null))
} }
assertThat(successful).isTrue() assertThat(successful).isTrue()
} }
@ -300,11 +308,11 @@ class InteractiveShellIntegrationTest {
} }
} }
val ansiProgressRenderer = mock<ANSIProgressRenderer> { val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
} }
InteractiveShell.runFlowByNameFragment( runFlowByNameFragment(
"NoOpFlowA", "NoOpFlowA",
"", output, node.rpc, ansiProgressRenderer) "", output, node.rpc, ansiProgressRenderer, createYamlInputMapper(node.rpc, null))
} }
assertThat(successful).isTrue() assertThat(successful).isTrue()
} }
@ -332,11 +340,11 @@ class InteractiveShellIntegrationTest {
} }
} }
val ansiProgressRenderer = mock<ANSIProgressRenderer> { val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
} }
InteractiveShell.runFlowByNameFragment( runFlowByNameFragment(
"NoOpFlo", "NoOpFlo",
"", output, node.rpc, ansiProgressRenderer) "", output, node.rpc, ansiProgressRenderer, createYamlInputMapper(node.rpc, null))
} }
assertThat(successful).isTrue() assertThat(successful).isTrue()
} }
@ -364,11 +372,43 @@ class InteractiveShellIntegrationTest {
} }
} }
val ansiProgressRenderer = mock<ANSIProgressRenderer> { val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() } on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
} }
InteractiveShell.runFlowByNameFragment( runFlowByNameFragment(
"Burble", "Burble",
"", output, node.rpc, ansiProgressRenderer) "", output, node.rpc, ansiProgressRenderer, createYamlInputMapper(node.rpc, null))
}
assertThat(successful).isTrue()
}
@Test
fun `shell should start flow with custom json deserializer`() {
val user = User("u", "p", setOf(all()))
var successful = false
driver(DriverParameters(notarySpecs = emptyList())) {
val nodeFuture = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), startInSameProcess = true)
val node = nodeFuture.getOrThrow()
val conf = ShellConfiguration(commandsDirectory = Files.createTempDir().toPath(),
user = user.username, password = user.password,
hostAndPort = node.rpcAddress)
InteractiveShell.startShell(conf)
// setup and configure some mocks required by InteractiveShell.runFlowByNameFragment()
val output = mock<RenderPrintWriter> {
on { println(any<String>()) } doAnswer {
val line = it.arguments[0]
println("$line")
if ((line is String) && (line.startsWith("Flow completed with result: ${FirstAndLastName("John", "Doe")}")))
successful = true
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
runFlowByNameFragment(
"DeserializeFlow",
"firstAndLastName: John Doe", output, node.rpc, ansiProgressRenderer, createYamlInputMapper(node.rpc, this::class.java.classLoader))
} }
assertThat(successful).isTrue() assertThat(successful).isTrue()
} }
@ -399,4 +439,37 @@ class BurbleFlow : FlowLogic<Unit>() {
override fun call() { override fun call() {
println("NO OP! (Burble)") println("NO OP! (Burble)")
} }
}
@CordaSerializable
data class FirstAndLastName(val firstName: String, val lastName: String)
@Suppress("UNUSED")
@StartableByRPC
class DeserializeFlow(private val firstAndLastName: FirstAndLastName) : FlowLogic<FirstAndLastName>() {
override val progressTracker = ProgressTracker()
override fun call(): FirstAndLastName {
return firstAndLastName
}
}
@Suppress("UNUSED")
class MyCustomJackson : CustomShellSerializationFactory {
override fun createJacksonModule() = object : SimpleModule() {
override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.setMixInAnnotations(FirstAndLastName::class.java, FirstLastNameMixin::class.java)
}
}
@JsonDeserialize(using = FirstLastNameDeserializer::class)
private interface FirstLastNameMixin
object FirstLastNameDeserializer : FromStringDeserializer<FirstAndLastName>(FirstAndLastName::class.java) {
override fun _deserialize(value: String, ctxt: DeserializationContext?): FirstAndLastName {
val (firstName, lastName) = value.split(" ")
return FirstAndLastName(firstName, lastName)
}
}
} }

View File

@ -7,7 +7,7 @@ import org.crsh.auth.AuthInfo
import org.crsh.auth.AuthenticationPlugin import org.crsh.auth.AuthenticationPlugin
import org.crsh.plugin.CRaSHPlugin import org.crsh.plugin.CRaSHPlugin
class CordaAuthenticationPlugin(private val rpcOps: (username: String, credential: String) -> CordaRPCOps) : CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> { class CordaAuthenticationPlugin(private val rpcOps: (username: String, credential: String) -> CordaRPCOps, private val classLoader: ClassLoader?) : CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> {
companion object { companion object {
private val logger = loggerFor<CordaAuthenticationPlugin>() private val logger = loggerFor<CordaAuthenticationPlugin>()
@ -24,7 +24,7 @@ class CordaAuthenticationPlugin(private val rpcOps: (username: String, credentia
} }
try { try {
val ops = rpcOps(username, credential) val ops = rpcOps(username, credential)
return CordaSSHAuthInfo(true, ops, isSsh = true) return CordaSSHAuthInfo(true, ops, classLoader, isSsh = true)
} catch (e: ActiveMQSecurityException) { } catch (e: ActiveMQSecurityException) {
logger.warn(e.message) logger.warn(e.message)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -6,10 +6,10 @@ import net.corda.tools.shell.InteractiveShell.createYamlInputMapper
import net.corda.tools.shell.utlities.ANSIProgressRenderer import net.corda.tools.shell.utlities.ANSIProgressRenderer
import org.crsh.auth.AuthInfo import org.crsh.auth.AuthInfo
class CordaSSHAuthInfo(val successful: Boolean, val rpcOps: CordaRPCOps, val ansiProgressRenderer: ANSIProgressRenderer? = null, val isSsh: Boolean = false) : AuthInfo { class CordaSSHAuthInfo(val successful: Boolean, val rpcOps: CordaRPCOps, val classLoader: ClassLoader?, val ansiProgressRenderer: ANSIProgressRenderer? = null, val isSsh: Boolean = false) : AuthInfo {
override fun isSuccessful(): Boolean = successful override fun isSuccessful(): Boolean = successful
val yamlInputMapper: ObjectMapper by lazy { val yamlInputMapper: ObjectMapper by lazy {
createYamlInputMapper(rpcOps) createYamlInputMapper(rpcOps, classLoader)
} }
} }

View File

@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator
import io.github.classgraph.ClassGraph
import net.corda.client.jackson.JacksonSupport import net.corda.client.jackson.JacksonSupport
import net.corda.client.jackson.StringToMethodCallParser import net.corda.client.jackson.StringToMethodCallParser
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
@ -24,6 +25,7 @@ import net.corda.core.internal.*
import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.client.jackson.internal.CustomShellSerializationFactory
import net.corda.tools.shell.utlities.ANSIProgressRenderer import net.corda.tools.shell.utlities.ANSIProgressRenderer
import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer
import org.crsh.command.InvocationContext import org.crsh.command.InvocationContext
@ -183,7 +185,7 @@ object InteractiveShell {
// Don't use the Java language plugin (we may not have tools.jar available at runtime), this // Don't use the Java language plugin (we may not have tools.jar available at runtime), this
// will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that // will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that
// is only the 'jmx' command. // is only the 'jmx' command.
return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps) return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps, InteractiveShell.classLoader)
} }
} }
val attributes = emptyMap<String, Any>() val attributes = emptyMap<String, Any>()
@ -192,7 +194,8 @@ object InteractiveShell {
this.config = config this.config = config
start(context) start(context)
ops = makeRPCOps(rpcOps, localUserName, localUserPassword) ops = makeRPCOps(rpcOps, localUserName, localUserPassword)
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, ops, StdoutANSIProgressRenderer)) return context.getPlugin(ShellFactory::class.java)
.create(null, CordaSSHAuthInfo(false, ops, InteractiveShell.classLoader, StdoutANSIProgressRenderer))
} }
} }
@ -212,7 +215,7 @@ object InteractiveShell {
return outputFormat return outputFormat
} }
fun createYamlInputMapper(rpcOps: CordaRPCOps): ObjectMapper { fun createYamlInputMapper(rpcOps: CordaRPCOps, classLoader: ClassLoader?): ObjectMapper {
// Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra // Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra
// serializers. // serializers.
return JacksonSupport.createDefaultMapper(rpcOps, YAMLFactory(), true).apply { return JacksonSupport.createDefaultMapper(rpcOps, YAMLFactory(), true).apply {
@ -220,6 +223,15 @@ object InteractiveShell {
addDeserializer(InputStream::class.java, InputStreamDeserializer) addDeserializer(InputStream::class.java, InputStreamDeserializer)
addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer) addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer)
} }
if(classLoader != null){
// Scan for CustomShellSerializationFactory on the classloader and register them as modules.
val customModules = ClassGraph().addClassLoader(classLoader).enableClassInfo().pooledScan().use { scan ->
scan.getClassesImplementing(CustomShellSerializationFactory::class.java.name)
.map { it.loadClass().newInstance() as CustomShellSerializationFactory }
}
log.info("Found the following custom shell serialization modules: ${customModules.map { it.javaClass.name }}")
customModules.forEach { registerModule(it.createJacksonModule()) }
}
registerModule(rpcModule) registerModule(rpcModule)
} }
} }
@ -262,7 +274,7 @@ object InteractiveShell {
output: RenderPrintWriter, output: RenderPrintWriter,
rpcOps: CordaRPCOps, rpcOps: CordaRPCOps,
ansiProgressRenderer: ANSIProgressRenderer, ansiProgressRenderer: ANSIProgressRenderer,
inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) { inputObjectMapper: ObjectMapper) {
val matches = try { val matches = try {
rpcOps.registeredFlows().filter { nameFragment in it } rpcOps.registeredFlows().filter { nameFragment in it }
} catch (e: PermissionException) { } catch (e: PermissionException) {
@ -359,7 +371,7 @@ object InteractiveShell {
fun killFlowById(id: String, fun killFlowById(id: String,
output: RenderPrintWriter, output: RenderPrintWriter,
rpcOps: CordaRPCOps, rpcOps: CordaRPCOps,
inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) { inputObjectMapper: ObjectMapper) {
try { try {
val runId = try { val runId = try {
inputObjectMapper.readValue(id, StateMachineRunId::class.java) inputObjectMapper.readValue(id, StateMachineRunId::class.java)