mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-2773: Support for custom Jackson serializers (#5025)
This commit is contained in:
parent
7b0d177a34
commit
f7bf1b41e0
@ -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 }
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user