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. */
val availableCommands: Map<String, String>
get() {
return methodMap.entries().map { entry ->
val (name, args) = entry // TODO: Kotlin 1.1
return methodMap.entries().map { (name, args) ->
val argStr = if (args.parameterCount == 0) "" else {
val paramNames = methodParamNames[name]!!
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
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.jcraft.jsch.ChannelExec
import com.jcraft.jsch.JSch
import com.nhaarman.mockito_kotlin.any
import com.nhaarman.mockito_kotlin.doAnswer
import com.nhaarman.mockito_kotlin.mock
import net.corda.client.jackson.internal.CustomShellSerializationFactory
import net.corda.client.rpc.RPCException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.div
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.getOrThrow
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.internal.useSslRpcOverrides
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 org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThat
@ -268,11 +276,11 @@ class InteractiveShellIntegrationTest {
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
InteractiveShell.runFlowByNameFragment(
runFlowByNameFragment(
"NoOpFlow",
"", output, node.rpc, ansiProgressRenderer)
"", output, node.rpc, ansiProgressRenderer, createYamlInputMapper(node.rpc, null))
}
assertThat(successful).isTrue()
}
@ -300,11 +308,11 @@ class InteractiveShellIntegrationTest {
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
InteractiveShell.runFlowByNameFragment(
runFlowByNameFragment(
"NoOpFlowA",
"", output, node.rpc, ansiProgressRenderer)
"", output, node.rpc, ansiProgressRenderer, createYamlInputMapper(node.rpc, null))
}
assertThat(successful).isTrue()
}
@ -332,11 +340,11 @@ class InteractiveShellIntegrationTest {
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
InteractiveShell.runFlowByNameFragment(
runFlowByNameFragment(
"NoOpFlo",
"", output, node.rpc, ansiProgressRenderer)
"", output, node.rpc, ansiProgressRenderer, createYamlInputMapper(node.rpc, null))
}
assertThat(successful).isTrue()
}
@ -364,11 +372,43 @@ class InteractiveShellIntegrationTest {
}
}
val ansiProgressRenderer = mock<ANSIProgressRenderer> {
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
}
InteractiveShell.runFlowByNameFragment(
runFlowByNameFragment(
"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()
}
@ -399,4 +439,37 @@ class BurbleFlow : FlowLogic<Unit>() {
override fun call() {
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.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 {
private val logger = loggerFor<CordaAuthenticationPlugin>()
@ -24,7 +24,7 @@ class CordaAuthenticationPlugin(private val rpcOps: (username: String, credentia
}
try {
val ops = rpcOps(username, credential)
return CordaSSHAuthInfo(true, ops, isSsh = true)
return CordaSSHAuthInfo(true, ops, classLoader, isSsh = true)
} catch (e: ActiveMQSecurityException) {
logger.warn(e.message)
} catch (e: Exception) {

View File

@ -6,10 +6,10 @@ import net.corda.tools.shell.InteractiveShell.createYamlInputMapper
import net.corda.tools.shell.utlities.ANSIProgressRenderer
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
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.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator
import io.github.classgraph.ClassGraph
import net.corda.client.jackson.JacksonSupport
import net.corda.client.jackson.StringToMethodCallParser
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.openFuture
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.StdoutANSIProgressRenderer
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
// will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that
// 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>()
@ -192,7 +194,8 @@ object InteractiveShell {
this.config = config
start(context)
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
}
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
// serializers.
return JacksonSupport.createDefaultMapper(rpcOps, YAMLFactory(), true).apply {
@ -220,6 +223,15 @@ object InteractiveShell {
addDeserializer(InputStream::class.java, InputStreamDeserializer)
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)
}
}
@ -262,7 +274,7 @@ object InteractiveShell {
output: RenderPrintWriter,
rpcOps: CordaRPCOps,
ansiProgressRenderer: ANSIProgressRenderer,
inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) {
inputObjectMapper: ObjectMapper) {
val matches = try {
rpcOps.registeredFlows().filter { nameFragment in it }
} catch (e: PermissionException) {
@ -359,7 +371,7 @@ object InteractiveShell {
fun killFlowById(id: String,
output: RenderPrintWriter,
rpcOps: CordaRPCOps,
inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) {
inputObjectMapper: ObjectMapper) {
try {
val runId = try {
inputObjectMapper.readValue(id, StateMachineRunId::class.java)