diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt index fb4becec14..57f5c8205b 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt @@ -184,6 +184,21 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor( throw UnparseableCallException("No overloads of the method matched") // Should be unreachable! } + /** + * Validates that the argument string matches the constructor parameters, i.e. this is a matching constructor + * for the argument string. Exception is thrown if not a match + * + * @param methodNameHint A name that will be used in exceptions if thrown; not used for any other purpose. + * @throws UnparseableCallException If no match is found between constructor parameters and passed string. + */ + @Throws(UnparseableCallException::class) + fun validateIsMatchingCtor(methodNameHint: String, parameters: List<Pair<String, Type>>, args: String) { + val tree = createJsonTreeAndValidate(methodNameHint, parameters, args) + val inOrderParams: List<Any?> = parameters.mapIndexed { _, (argName, argType) -> + tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args) + } + } + /** * Parses only the arguments string given the info about parameter names and types. * @@ -191,10 +206,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor( */ @Throws(UnparseableCallException::class) 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 tree = createJsonTreeAndValidate(methodNameHint, parameters, 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) @@ -213,6 +225,14 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor( return inOrderParams.toTypedArray() } + private fun createJsonTreeAndValidate(methodNameHint: String, parameters: List<Pair<String, Type>>, args: String) : JsonNode { + // 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) + return tree + } + /** Returns a string-to-string map of commands to a string describing available parameter types. */ val availableCommands: Map<String, String> get() { diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 86444f179f..4d177363d3 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -389,41 +389,60 @@ object InteractiveShell { inputData: String, clazz: Class<out FlowLogic<T>>, om: ObjectMapper): FlowProgressHandle<T> { - // For each constructor, attempt to parse the input data as a method call. Use the first that succeeds, - // and keep track of the reasons we failed so we can print them out if no constructors are usable. - val parser = StringToMethodCallParser(clazz, om) - val errors = ArrayList<String>() + val errors = ArrayList<String>() + val parser = StringToMethodCallParser(clazz, om) + val nameTypeList = getMatchingConstructorParamsAndTypes(parser, inputData, clazz) + + try { + val args = parser.parseArguments(clazz.name, nameTypeList, inputData) + return invoke(clazz, args) + } catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { + val argTypes = nameTypeList.map { (_, type) -> type } + errors.add("$argTypes: <constructor missing parameter reflection data>") + } catch (e: StringToMethodCallParser.UnparseableCallException) { + val argTypes = nameTypeList.map { (_, type) -> type } + errors.add("$argTypes: ${e.message}") + } + throw NoApplicableConstructor(errors) + } + + private fun <T> getMatchingConstructorParamsAndTypes(parser: StringToMethodCallParser<FlowLogic<T>>, + inputData: String, + clazz: Class<out FlowLogic<T>>) : List<Pair<String, Type>> { + val errors = ArrayList<String>() val classPackage = clazz.packageName - for (ctor in clazz.constructors) { - var paramNamesFromConstructor: List<String>? = null + lateinit var paramNamesFromConstructor: List<String> + + for (ctor in clazz.constructors) { // Attempt construction with the given arguments. fun getPrototype(): List<String> { - val argTypes = ctor.genericParameterTypes.map { it: Type -> + val argTypes = ctor.genericParameterTypes.map { // If the type name is in the net.corda.core or java namespaces, chop off the package name // because these hierarchies don't have (m)any ambiguous names and the extra detail is just noise. maybeAbbreviateGenericType(it, classPackage) } - return paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" } + return paramNamesFromConstructor.zip(argTypes).map { (name, type) -> "$name: $type" } } try { - // Attempt construction with the given arguments. paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor) - val args = parser.parseArguments(clazz.name, paramNamesFromConstructor.zip(ctor.genericParameterTypes), inputData) - if (args.size != ctor.genericParameterTypes.size) { - errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.genericParameterTypes.size} needed)") - continue - } - return invoke(clazz, args) - } catch (e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { + val nameTypeList = paramNamesFromConstructor.zip(ctor.genericParameterTypes) + parser.validateIsMatchingCtor(clazz.name, nameTypeList, inputData) + return nameTypeList + + } + catch (e: StringToMethodCallParser.UnparseableCallException.MissingParameter) { errors.add("${getPrototype()}: missing parameter ${e.paramName}") - } catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) { + } + catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) { errors.add("${getPrototype()}: too many parameters") - } catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { + } + catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { val argTypes = ctor.genericParameterTypes.map { it.typeName } errors.add("$argTypes: <constructor missing parameter reflection data>") - } catch (e: StringToMethodCallParser.UnparseableCallException) { + } + catch (e: StringToMethodCallParser.UnparseableCallException) { val argTypes = ctor.genericParameterTypes.map { it.typeName } errors.add("$argTypes: ${e.message}") } diff --git a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java index e3dde61bb6..5d7cabe089 100644 --- a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java +++ b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java @@ -30,6 +30,7 @@ import java.util.*; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class InteractiveShellJavaTest { private static TestIdentity megaCorp = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB")); @@ -259,4 +260,14 @@ public class InteractiveShellJavaTest { assertEquals("[amount: Amount<Currency>, abc: int]: missing parameter abc", e.getErrors().get(1)); } } + + @Test + public void flowStartWithUnknownParty() throws InteractiveShell.NoApplicableConstructor { + try { + check("party: nonexistent", "", FlowA.class); + } catch (InteractiveShell.NoApplicableConstructor e) { + assertTrue(e.getErrors().get(0).contains("No matching Party found")); + assertEquals(1, e.getErrors().size()); + } + } } diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt index df52553a74..e0492d5c9f 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt @@ -154,7 +154,7 @@ class InteractiveShellTest { @Test fun flowStartWithArrayType() = check( - input = "b: [ One, Two, Three, Four ]", + input = "c: [ One, Two, Three, Four ]", expected = "One+Two+Three+Four" ) @@ -174,7 +174,7 @@ class InteractiveShellTest { fun flowStartNoArgs() = check("", "") @Test(expected = InteractiveShell.NoApplicableConstructor::class) - fun flowMissingParam() = check("c: Yo", "") + fun flowMissingParam() = check("d: Yo", "") @Test(expected = InteractiveShell.NoApplicableConstructor::class) fun flowTooManyParams() = check("b: 12, c: Yo, d: Bar", "") @@ -190,7 +190,7 @@ class InteractiveShellTest { "[pair: Pair<Amount<Currency>, SecureHash.SHA256>]: missing parameter pair", "[party: Party]: missing parameter party", "[b: Integer, amount: Amount<UserValue>]: missing parameter b", - "[b: String[]]: missing parameter b", + "[c: String[]]: missing parameter c", "[b: Integer, c: String]: missing parameter b", "[a: String]: missing parameter a", "[b: Integer]: missing parameter b" @@ -275,7 +275,7 @@ class FlowA(val a: String) : FlowLogic<String>() { 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(c: Array<String>) : this(c.joinToString("+")) constructor(amounts: Array<Amount<UserValue>>) : this(amounts.joinToString("++", transform = Amount<UserValue>::toString)) override val progressTracker = ProgressTracker()