CORDA-1907: Allow Corda's shell to deserialise using generic type information. (#3810) (#3827)

This commit is contained in:
Chris Rankin 2018-08-22 11:43:41 +01:00 committed by Michele Sollecito
parent f5f1564205
commit d15efcec10
5 changed files with 87 additions and 17 deletions

View File

@ -5905,7 +5905,7 @@ public class net.corda.client.jackson.StringToMethodCallParser extends java.lang
@NotNull
public final net.corda.client.jackson.StringToMethodCallParser<T>$ParsedMethodCall parse(T, String)
@NotNull
public final Object[] parseArguments(String, java.util.List<? extends kotlin.Pair<String, ? extends Class<?>>>, String)
public final Object[] parseArguments(String, java.util.List<? extends kotlin.Pair<String, ? extends reflect.Type>>, String)
public static final net.corda.client.jackson.StringToMethodCallParser$Companion Companion
##
public static final class net.corda.client.jackson.StringToMethodCallParser$Companion extends java.lang.Object

View File

@ -10,6 +10,7 @@ import net.corda.core.CordaException
import net.corda.core.utilities.contextLogger
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.lang.reflect.Type
import java.util.concurrent.Callable
import javax.annotation.concurrent.ThreadSafe
import kotlin.reflect.KClass
@ -173,7 +174,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
// and fail for that too.
for ((index, method) in methods.withIndex()) {
try {
val args = parseArguments(name, paramNamesFromMethod(method).zip(method.parameterTypes), argStr)
val args = parseArguments(name, paramNamesFromMethod(method).zip(method.genericParameterTypes), argStr)
return ParsedMethodCall(target, method, args)
} catch (e: UnparseableCallException) {
if (index == methods.size - 1)
@ -189,15 +190,16 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
* @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?> {
fun parseArguments(methodNameHint: String, parameters: List<Pair<String, Type>>, 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 { _, (argName, argType) ->
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
val entryType = om.typeFactory.constructType(argType)
try {
om.readValue(entry.traverse(om), argType)
om.readValue<Any>(entry.traverse(om), entryType)
} catch (e: Exception) {
throw UnparseableCallException.FailedParse(e)
}

View File

@ -3,8 +3,9 @@ package net.corda.client.jackson
import net.corda.core.crypto.SecureHash
import org.junit.Assert.assertArrayEquals
import org.junit.Test
import kotlin.reflect.full.primaryConstructor
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class StringToMethodCallParserTest {
@Suppress("UNUSED")
@ -14,6 +15,7 @@ class StringToMethodCallParserTest {
fun twoStrings(a: String, b: String) = a + b
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()
fun complexObject(pair: Pair<Int, String>) = pair
fun complexNestedObject(pairs: Pair<Int, Deque<Char>>) = pairs
fun overload(a: String) = a
fun overload(a: String, b: String) = a + b
@ -39,30 +41,68 @@ class StringToMethodCallParserTest {
}
}
/*
* It would be unreasonable to expect "[ A, B, C ]" to deserialise as "Deque<Char>" by default.
* Deque is chosen as we still expect it to preserve the order of its elements.
*/
@Test
fun complexNestedGenericMethod() {
val parser = StringToMethodCallParser(Target::class)
val result = parser.parse(Target(), "complexNestedObject pairs: { first: 101, second: [ A, B, C ] }").invoke()
assertTrue(result is Pair<*,*>)
result as Pair<*,*>
assertEquals(101, result.first)
assertTrue(result.second is Deque<*>)
val deque = result.second as Deque<*>
assertArrayEquals(arrayOf('A', 'B', 'C'), deque.toTypedArray())
}
@Suppress("UNUSED")
class ConstructorTarget(val someWord: String, val aDifferentThing: Int) {
constructor(alternativeWord: String) : this(alternativeWord, 0)
constructor(numbers: List<Long>) : this(numbers.map(Long::toString).joinToString("+"), numbers.size)
}
@Test
fun ctor1() {
val clazz = ConstructorTarget::class.java
val parser = StringToMethodCallParser(clazz)
val ctor = clazz.constructors.single { it.parameterCount == 2 }
val ctor = clazz.getDeclaredConstructor(String::class.java, Int::class.java)
val names: List<String> = parser.paramNamesFromConstructor(ctor)
assertEquals(listOf("someWord", "aDifferentThing"), names)
val args: Array<Any?> = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "someWord: Blah blah blah, aDifferentThing: 12")
assertArrayEquals(args, arrayOf<Any?>("Blah blah blah", 12))
assertArrayEquals(arrayOf("Blah blah blah", 12), args)
}
@Test
fun ctor2() {
val clazz = ConstructorTarget::class.java
val parser = StringToMethodCallParser(clazz)
val ctor = clazz.constructors.single { it.parameterCount == 1 }
val ctor = clazz.getDeclaredConstructor(String::class.java)
val names: List<String> = parser.paramNamesFromConstructor(ctor)
assertEquals(listOf("alternativeWord"), names)
val args: Array<Any?> = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "alternativeWord: Foo bar!")
assertArrayEquals(args, arrayOf<Any?>("Foo bar!"))
assertArrayEquals(arrayOf("Foo bar!"), args)
}
@Test
fun constructorWithGenericArgs() {
val clazz = ConstructorTarget::class.java
val ctor = clazz.getDeclaredConstructor(List::class.java)
StringToMethodCallParser(clazz).apply {
val names = paramNamesFromConstructor(ctor)
assertEquals(listOf("numbers"), names)
val commandLine = "numbers: [ 1, 2, 3 ]"
val args = parseArguments(clazz.name, names.zip(ctor.parameterTypes), commandLine)
assertArrayEquals(arrayOf(listOf(1, 2, 3)), args)
val trueArgs = parseArguments(clazz.name, names.zip(ctor.genericParameterTypes), commandLine)
assertArrayEquals(arrayOf(listOf(1L, 2L, 3L)), trueArgs)
}
}
}

View File

@ -290,7 +290,7 @@ object InteractiveShell {
for (ctor in clazz.constructors) {
var paramNamesFromConstructor: List<String>? = null
fun getPrototype(): List<String> {
val argTypes = ctor.parameterTypes.map { it.simpleName }
val argTypes = ctor.genericParameterTypes.map { it.typeName }
return paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" }
}
@ -298,10 +298,10 @@ object InteractiveShell {
// Attempt construction with the given arguments.
val args = database.transaction {
paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor)
parser.parseArguments(clazz.name, paramNamesFromConstructor!!.zip(ctor.parameterTypes), inputData)
parser.parseArguments(clazz.name, paramNamesFromConstructor!!.zip(ctor.genericParameterTypes), inputData)
}
if (args.size != ctor.parameterTypes.size) {
errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.parameterTypes.size} needed)")
if (args.size != ctor.genericParameterTypes.size) {
errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.genericParameterTypes.size} needed)")
continue
}
val flow = ctor.newInstance(*args) as FlowLogic<*>
@ -315,10 +315,10 @@ object InteractiveShell {
} catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) {
errors.add("${getPrototype()}: too many parameters")
} catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) {
val argTypes = ctor.parameterTypes.map { it.simpleName }
val argTypes = ctor.genericParameterTypes.map { it.typeName }
errors.add("$argTypes: <constructor missing parameter reflection data>")
} catch (e: StringToMethodCallParser.UnparseableCallException) {
val argTypes = ctor.parameterTypes.map { it.simpleName }
val argTypes = ctor.genericParameterTypes.map { it.typeName }
errors.add("$argTypes: ${e.message}")
}
}

View File

@ -1,7 +1,9 @@
package net.corda.node
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import net.corda.client.jackson.JacksonSupport
import net.corda.client.jackson.internal.ToStringSerialize
import net.corda.core.contracts.Amount
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
@ -47,6 +49,9 @@ class InteractiveShellTest {
constructor(amount: Amount<Currency>) : this(amount.toString())
constructor(pair: Pair<Amount<Currency>, SecureHash.SHA256>) : this(pair.toString())
constructor(party: Party) : this(party.name.toString())
constructor(b: Int?, amount: Amount<UserValue>) : this("${(b ?: 0) + amount.quantity} ${amount.token}")
constructor(b: Array<String>) : this(b.joinToString("+"))
constructor(amounts: Array<Amount<UserValue>>) : this(amounts.map(Amount<UserValue>::toString).joinToString("++"))
override val progressTracker = ProgressTracker()
override fun call() = a
@ -80,8 +85,26 @@ class InteractiveShellTest {
@Test
fun flowStartWithNestedTypes() = check(
"pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }",
"($100.12, df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587)"
input = "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }",
expected = "(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)"
)
@Test
fun flowStartWithArrayType() = check(
input = "b: [ One, Two, Three, Four ]",
expected = "One+Two+Three+Four"
)
@Test
fun flowStartWithUserAmount() = check(
input = """b: 500, amount: { "quantity": 10001, "token":{ "label": "of value" } }""",
expected = "10501 of value"
)
@Test
fun flowStartWithArrayOfNestedTypes() = check(
input = """amounts: [ { "quantity": 10, "token": { "label": "(1)" } }, { "quantity": 200, "token": { "label": "(2)" } } ]""",
expected = "10 (1)++200 (2)"
)
@Test(expected = InteractiveShell.NoApplicableConstructor::class)
@ -95,4 +118,9 @@ class InteractiveShellTest {
@Test
fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString())
@ToStringSerialize
data class UserValue(@JsonProperty("label") val label: String) {
override fun toString() = label
}
}