mirror of
https://github.com/corda/corda.git
synced 2025-04-07 19:34:41 +00:00
Add a general string to method call parsing utility class to the Jackson module. It is useful for the shell work.
This commit is contained in:
parent
d576468a93
commit
b8a4c7bea3
@ -1,9 +1,6 @@
|
||||
package net.corda.jackson
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.fasterxml.jackson.core.*
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
|
||||
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
|
||||
@ -30,6 +27,7 @@ import java.util.*
|
||||
* Note that Jackson can also be used to serialise/deserialise other formats such as Yaml and XML.
|
||||
*/
|
||||
object JacksonSupport {
|
||||
// TODO: This API could use some tidying up - there should really only need to be one kind of mapper.
|
||||
// If you change this API please update the docs in the docsite (json.rst)
|
||||
|
||||
interface PartyObjectMapper {
|
||||
@ -37,15 +35,15 @@ object JacksonSupport {
|
||||
fun partyFromKey(owningKey: CompositeKey): Party?
|
||||
}
|
||||
|
||||
class RpcObjectMapper(val rpc: CordaRPCOps) : PartyObjectMapper, ObjectMapper() {
|
||||
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
|
||||
override fun partyFromKey(owningKey: CompositeKey): Party? = rpc.partyFromKey(owningKey)
|
||||
}
|
||||
class IdentityObjectMapper(val identityService: IdentityService) : PartyObjectMapper, ObjectMapper(){
|
||||
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
|
||||
override fun partyFromKey(owningKey: CompositeKey): Party? = identityService.partyFromKey(owningKey)
|
||||
}
|
||||
class NoPartyObjectMapper: PartyObjectMapper, ObjectMapper() {
|
||||
class NoPartyObjectMapper(factory: JsonFactory): PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromKey(owningKey: CompositeKey): Party? = throw UnsupportedOperationException()
|
||||
}
|
||||
@ -59,7 +57,9 @@ object JacksonSupport {
|
||||
addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||
addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
|
||||
addSerializer(SecureHash::class.java, SecureHashSerializer)
|
||||
addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer)
|
||||
addDeserializer(SecureHash::class.java, SecureHashDeserializer())
|
||||
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||
|
||||
// For ed25519 pubkeys
|
||||
@ -86,16 +86,16 @@ object JacksonSupport {
|
||||
}
|
||||
|
||||
/** Mapper requiring RPC support to deserialise parties from names */
|
||||
@JvmStatic
|
||||
fun createDefaultMapper(rpc: CordaRPCOps): ObjectMapper = configureMapper(RpcObjectMapper(rpc))
|
||||
@JvmStatic @JvmOverloads
|
||||
fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory))
|
||||
|
||||
/** For testing or situations where deserialising parties is not required */
|
||||
@JvmStatic
|
||||
fun createNonRpcMapper(): ObjectMapper = configureMapper(NoPartyObjectMapper())
|
||||
@JvmStatic @JvmOverloads
|
||||
fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory))
|
||||
|
||||
/** For testing with an in memory identity service */
|
||||
@JvmStatic
|
||||
fun createInMemoryMapper(identityService: IdentityService) = configureMapper(IdentityObjectMapper(identityService))
|
||||
@JvmStatic @JvmOverloads
|
||||
fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory()) = configureMapper(IdentityObjectMapper(identityService, factory))
|
||||
|
||||
private fun configureMapper(mapper: ObjectMapper): ObjectMapper = mapper.apply {
|
||||
enable(SerializationFeature.INDENT_OUTPUT)
|
||||
|
@ -0,0 +1,184 @@
|
||||
package net.corda.jackson
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import net.corda.jackson.StringToMethodCallParser.ParsedMethodCall
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Method
|
||||
import java.util.concurrent.Callable
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KotlinReflectionInternalError
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
|
||||
/**
|
||||
* This class parses strings in a format designed for human usability into [ParsedMethodCall] objects representing a
|
||||
* ready-to-invoke call on the given target object. The strings accepted by this class are a minor variant of
|
||||
* [Yaml](http://www.yaml.org/spec/1.2/spec.html) and can be easily typed at a command line. Intended use cases include
|
||||
* things like the Corda shell, text-based RPC dispatch, simple scripting and so on.
|
||||
*
|
||||
* # Syntax
|
||||
*
|
||||
* The format of the string is as follows. The first word is the name of the method and must always be present. The rest,
|
||||
* which is optional, is wrapped in curly braces and parsed as if it were a Yaml object. The keys of this object are then
|
||||
* mapped to the parameters of the method via the usual Jackson mechanisms. The standard [java.lang.Object] methods are
|
||||
* excluded.
|
||||
*
|
||||
* One convenient feature of Yaml is that barewords collapse into strings, thus you can write a call like the following:
|
||||
*
|
||||
* fun someCall(note: String, option: Boolean)
|
||||
*
|
||||
* someCall note: This is a really helpful feature, option: true
|
||||
*
|
||||
* ... and it will be parsed in the intuitive way. Quotes are only needed if you want to put a comma into the string.
|
||||
*
|
||||
* There is an [online Yaml parser](http://yaml-online-parser.appspot.com/) which can be used to explore
|
||||
* the allowed syntax.
|
||||
*
|
||||
* # Usage
|
||||
*
|
||||
* This class is thread safe. Multiple strings may be parsed in parallel, and the resulting [ParsedMethodCall]
|
||||
* objects may be reused multiple times and also invoked in parallel, as long as the underling target object is
|
||||
* thread safe itself.
|
||||
*
|
||||
* You may pass in an alternative [ObjectMapper] to control what types can be parsed, but it must be configured
|
||||
* with the [YAMLFactory] for the class to work.
|
||||
*
|
||||
* # Limitations
|
||||
*
|
||||
* - The target class must be either a Kotlin class, or a Java class compiled with the -parameters command line
|
||||
* switch, as the class relies on knowing the names of parameters which isn't data provided by default by the
|
||||
* Java compiler.
|
||||
* - Vararg methods are not supported, as the type information that'd be required is missing.
|
||||
* - Method overloads that have identical parameter names but different types can't be handled, because often
|
||||
* a string could map to multiple types, so which one to use is ambiguous. If you want your interface to be
|
||||
* usable with this utility make sure the parameter and method names don't rely on type overloading.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* fun simple() = ...
|
||||
* "simple" -> runs the no-args function 'simple'
|
||||
*
|
||||
* fun attachmentExists(id: SecureHash): Boolean
|
||||
* "attachmentExists id: b6d7e826e87" -> parses the given ID as a SecureHash
|
||||
*
|
||||
* fun addNote(id: SecureHash, note: String)
|
||||
* "addNote id: b6d7e826e8739ab2eb6e077fc4fba9b04fb880bb4cbd09bc618d30234a8827a4, note: Some note"
|
||||
*/
|
||||
@ThreadSafe
|
||||
open class StringToMethodCallParser<in T : Any>(targetType: Class<out T>,
|
||||
private val om: ObjectMapper = JacksonSupport.createNonRpcMapper(YAMLFactory())) {
|
||||
/** Same as the regular constructor but takes a Kotlin reflection [KClass] instead of a Java [Class]. */
|
||||
constructor(targetType: KClass<out T>) : this(targetType.java)
|
||||
|
||||
companion object {
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
private val ignoredNames = Object::class.java.methods.map { it.name }
|
||||
private fun methodsFromType(clazz: Class<*>) = clazz.methods.map { it.name to it }.toMap().filterKeys { it !in ignoredNames }
|
||||
private val log = LoggerFactory.getLogger(StringToMethodCallParser::class.java)!!
|
||||
}
|
||||
|
||||
/** The methods that can be invoked via this parser. */
|
||||
protected val methodMap = methodsFromType(targetType)
|
||||
/** A map of method name to parameter names for the target type. */
|
||||
val methodParamNames: Map<String, List<String>> = targetType.declaredMethods.mapNotNull {
|
||||
try {
|
||||
it.name to paramNamesFromMethod(it)
|
||||
} catch(e: KotlinReflectionInternalError) {
|
||||
// Kotlin reflection doesn't support every method that can exist on an object (in particular, reified
|
||||
// inline methods) so we just ignore those here.
|
||||
null
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
inner class ParsedMethodCall(private val target: T?, val methodName: String, val args: Array<Any?>) : Callable<Any?> {
|
||||
operator fun invoke(): Any? = call()
|
||||
override fun call(): Any? {
|
||||
if (target == null)
|
||||
throw IllegalStateException("No target object was specified")
|
||||
if (log.isDebugEnabled)
|
||||
log.debug("Invoking call $methodName($args)")
|
||||
return methodMap[methodName]!!.invoke(target, *args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses either Kotlin or Java 8 reflection to learn the names of the parameters to a method.
|
||||
*/
|
||||
open fun paramNamesFromMethod(method: Method): List<String> {
|
||||
val kf: KFunction<*>? = method.kotlinFunction
|
||||
return method.parameters.mapIndexed { index, param ->
|
||||
when {
|
||||
param.isNamePresent -> param.name
|
||||
// index + 1 because the first Kotlin reflection param is 'this', but that doesn't match Java reflection.
|
||||
kf != null -> kf.parameters[index + 1].name ?: throw UnparseableCallException.ReflectionDataMissing(method.name, index)
|
||||
else -> throw UnparseableCallException.ReflectionDataMissing(method.name, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses either Kotlin or Java 8 reflection to learn the names of the parameters to a constructor.
|
||||
*/
|
||||
open fun paramNamesFromConstructor(ctor: Constructor<*>): List<String> {
|
||||
val kf: KFunction<*>? = ctor.kotlinFunction
|
||||
return ctor.parameters.mapIndexed { index, param ->
|
||||
when {
|
||||
param.isNamePresent -> param.name
|
||||
kf != null -> kf.parameters[index].name ?: throw UnparseableCallException.ReflectionDataMissing("<init>", index)
|
||||
else -> throw UnparseableCallException.ReflectionDataMissing("<init>", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class UnparseableCallException(command: String) : Exception("Could not parse as a command: $command") {
|
||||
class UnknownMethod(val methodName: String) : UnparseableCallException("Unknown command name: $methodName")
|
||||
class MissingParameter(methodName: String, val paramName: String, command: String) : UnparseableCallException("Parameter $paramName missing from attempt to invoke $methodName in command: $command")
|
||||
class TooManyParameters(methodName: String, command: String) : UnparseableCallException("Too many parameters provided for $methodName: $command")
|
||||
class ReflectionDataMissing(methodName: String, argIndex: Int) : UnparseableCallException("Method $methodName missing parameter name at index $argIndex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given command as a call on the target type. The target should be specified, if it's null then
|
||||
* the resulting [ParsedMethodCall] can't be invoked, just inspected.
|
||||
*/
|
||||
@Throws(UnparseableCallException::class)
|
||||
fun parse(target: T?, command: String): ParsedMethodCall {
|
||||
log.debug("Parsing call command from string: {}", command)
|
||||
val spaceIndex = command.indexOf(' ')
|
||||
val name = if (spaceIndex != -1) command.substring(0, spaceIndex) else command
|
||||
val argStr = if (spaceIndex != -1) command.substring(spaceIndex) else ""
|
||||
val method = methodMap[name] ?: throw UnparseableCallException.UnknownMethod(name)
|
||||
log.debug("Parsing call for method {}", name)
|
||||
val args = parseArguments(name, paramNamesFromMethod(method).zip(method.parameterTypes), argStr)
|
||||
return ParsedMethodCall(target, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses only the arguments string given the info about parameter names and types.
|
||||
*
|
||||
* @param methodNameHint A name that will be used in exceptions if thrown; not used for any other purpose.
|
||||
*/
|
||||
@Throws(UnparseableCallException::class)
|
||||
fun parseArguments(methodNameHint: String, parameters: List<Pair<String, Class<*>>>, args: String): Array<Any?> {
|
||||
// If we have parameters, wrap them in {} to allow the Yaml parser to eat them on a single line.
|
||||
val parameterString = "{ $args }"
|
||||
val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
|
||||
if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args)
|
||||
val inOrderParams: List<Any?> = parameters.mapIndexed { index, param ->
|
||||
val (argName, argType) = param
|
||||
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
|
||||
om.readValue(entry.traverse(om), argType)
|
||||
}
|
||||
if (log.isDebugEnabled) {
|
||||
inOrderParams.forEachIndexed { i, param ->
|
||||
val tmp = if (param != null) "${param.javaClass.name} -> $param" else "(null)"
|
||||
log.debug("Parameter $i. $tmp")
|
||||
}
|
||||
}
|
||||
return inOrderParams.toTypedArray()
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package net.corda.jackson
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class StringToMethodCallParserTest {
|
||||
@Suppress("UNUSED")
|
||||
class Target {
|
||||
fun simple() = "simple"
|
||||
fun string(note: String) = note
|
||||
fun twoStrings(a: String, b: String) = a + b
|
||||
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()!!
|
||||
fun complexObject(pair: Pair<Int, String>) = pair
|
||||
}
|
||||
|
||||
val randomHash = "361170110f61086f77ff2c5b7ab36513705da1a3ebabf14dbe5cc9c982c45401"
|
||||
val tests = mapOf(
|
||||
"simple" to "simple",
|
||||
"string note: A test of barewords" to "A test of barewords",
|
||||
"twoStrings a: Some words, b: ' and some words, like, Kirk, would, speak'" to "Some words and some words, like, Kirk, would, speak",
|
||||
"simpleObject hash: $randomHash" to randomHash.toUpperCase(),
|
||||
"complexObject pair: { first: 12, second: Word up brother }" to Pair(12, "Word up brother")
|
||||
)
|
||||
|
||||
@Test
|
||||
fun calls() {
|
||||
val parser = StringToMethodCallParser(Target::class)
|
||||
val target = Target()
|
||||
for ((input, output) in tests) {
|
||||
assertEquals(output, parser.parse(target, input).invoke())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package net.corda.testing.http
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.jackson.JacksonSupport
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@ -22,7 +22,7 @@ object HttpUtils {
|
||||
.readTimeout(60, TimeUnit.SECONDS).build()
|
||||
}
|
||||
val defaultMapper: ObjectMapper by lazy {
|
||||
ObjectMapper().registerModule(JacksonSupport.javaTimeModule).registerModule(KotlinModule())
|
||||
ObjectMapper().registerModule(JavaTimeModule()).registerModule(KotlinModule())
|
||||
}
|
||||
|
||||
fun putJson(url: URL, data: String) : Boolean {
|
||||
|
Loading…
x
Reference in New Issue
Block a user