diff --git a/pom.xml b/pom.xml
index e3dd6f02..4a8e842d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
spring-shell2-core
+ spring-shell2-core-tests
spring-shell2-standard
spring-shell2-standard-commands
spring-shell2-jcommander-adapter
@@ -36,6 +37,11 @@
spring-shell2-core
${project.version}
+
+ org.springframework.shell
+ spring-shell2-core-tests
+ ${project.version}
+
org.springframework.shell
spring-shell2-standard
diff --git a/spring-shell2-core-tests/pom.xml b/spring-shell2-core-tests/pom.xml
new file mode 100644
index 00000000..01dff2f5
--- /dev/null
+++ b/spring-shell2-core-tests/pom.xml
@@ -0,0 +1,27 @@
+
+ 4.0.0
+
+ spring-shell2-core-tests
+ jar
+
+
+ org.springframework.shell
+ spring-shell2-parent
+ 2.0.0.BUILD-SNAPSHOT
+
+
+ Core API test classes for Spring Shell 2
+
+
+
+ org.springframework.shell
+ spring-shell2-core
+
+
+ org.assertj
+ assertj-core
+ compile
+
+
+
diff --git a/spring-shell2-core-tests/src/main/java/org/springframework/shell2/ValueResultAsserts.java b/spring-shell2-core-tests/src/main/java/org/springframework/shell2/ValueResultAsserts.java
new file mode 100644
index 00000000..b6f85b1e
--- /dev/null
+++ b/spring-shell2-core-tests/src/main/java/org/springframework/shell2/ValueResultAsserts.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.shell2;
+
+import org.assertj.core.api.AbstractAssert;
+import org.assertj.core.api.Assertions;
+
+/**
+ * Assertions for {@link ValueResult}.
+ *
+ * @author Camilo Gonzalez
+ */
+public class ValueResultAsserts extends AbstractAssert {
+
+ public ValueResultAsserts(ValueResult actual) {
+ super(actual, ValueResultAsserts.class);
+ }
+
+ public ValueResultAsserts hasValue(Object expectedValue) {
+ isNotNull();
+ Assertions.assertThat(actual.resolvedValue()).isEqualTo(expectedValue);
+ return this;
+ }
+
+ public ValueResultAsserts usesWords(int... expectedWordsUsed) {
+ isNotNull();
+
+ Assertions.assertThat(actual.wordsUsed().stream().toArray()).containsExactly(expectedWordsUsed);
+
+ return this;
+ }
+
+ public ValueResultAsserts notUsesWords() {
+ isNotNull();
+ Assertions.assertThat(actual.wordsUsed().isEmpty());
+ return this;
+ }
+
+ public ValueResultAsserts usesWordsForValue(int... expectedWordsUsedForValue) {
+ isNotNull();
+
+ Assertions.assertThat(actual.wordsUsedForValue().stream().toArray()).containsExactly(expectedWordsUsedForValue);
+
+ return this;
+ }
+
+ public ValueResultAsserts notUsesWordsForValue() {
+ isNotNull();
+ Assertions.assertThat(actual.wordsUsedForValue().isEmpty());
+ return this;
+ }
+
+ public static ValueResultAsserts assertThat(ValueResult valueResult) {
+ return new ValueResultAsserts(valueResult);
+ }
+}
diff --git a/spring-shell2-core/src/main/java/org/springframework/shell2/ParameterResolver.java b/spring-shell2-core/src/main/java/org/springframework/shell2/ParameterResolver.java
index 6dcc12f4..53702ded 100644
--- a/spring-shell2-core/src/main/java/org/springframework/shell2/ParameterResolver.java
+++ b/spring-shell2-core/src/main/java/org/springframework/shell2/ParameterResolver.java
@@ -36,7 +36,7 @@ public interface ParameterResolver {
/**
* Turn the given textual input into an actual object, maybe using some conversion or lookup mechanism.
*/
- Object resolve(MethodParameter methodParameter, List words);
+ ValueResult resolve(MethodParameter methodParameter, List words);
/**
* Describe a supported parameter, so that integrated help can be generated.
diff --git a/spring-shell2-core/src/main/java/org/springframework/shell2/Shell.java b/spring-shell2-core/src/main/java/org/springframework/shell2/Shell.java
index 880ea325..8fd50ba6 100644
--- a/spring-shell2-core/src/main/java/org/springframework/shell2/Shell.java
+++ b/spring-shell2-core/src/main/java/org/springframework/shell2/Shell.java
@@ -222,7 +222,7 @@ public class Shell implements CommandRegistry {
Arrays.fill(args, UNRESOLVED);
for (int i = 0; i < parameters.length; i++) {
MethodParameter methodParameter = Utils.createMethodParameter(method, i);
- args[i] = findResolver(methodParameter).resolve(methodParameter, wordsForArgs);
+ args[i] = findResolver(methodParameter).resolve(methodParameter, wordsForArgs).resolvedValue();
}
return args;
}
diff --git a/spring-shell2-core/src/main/java/org/springframework/shell2/ValueResult.java b/spring-shell2-core/src/main/java/org/springframework/shell2/ValueResult.java
new file mode 100644
index 00000000..a86524d4
--- /dev/null
+++ b/spring-shell2-core/src/main/java/org/springframework/shell2/ValueResult.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.shell2;
+
+import java.util.BitSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.naming.spi.ResolveResult;
+
+import org.springframework.core.MethodParameter;
+
+/**
+ * A {@link ResolveResult} for a successful {@link ParameterResolver#resolve} operation.
+ *
+ * @author Camilo Gonzalez
+ */
+public class ValueResult {
+
+ private final MethodParameter methodParameter;
+
+ private final Object resolvedValue;
+
+ private final BitSet wordsUsed;
+
+ private final BitSet wordsUsedForValue;
+
+ public ValueResult(MethodParameter methodParameter, Object resolvedValue) {
+ this(methodParameter, resolvedValue, new BitSet(), new BitSet());
+ }
+
+ public ValueResult(MethodParameter methodParameter, Object resolvedValue, BitSet wordsUsed,
+ BitSet wordsUsedForValue) {
+
+ this.methodParameter = methodParameter;
+ this.resolvedValue = resolvedValue;
+ this.wordsUsed = wordsUsed == null ? new BitSet() : wordsUsed;
+ this.wordsUsedForValue = wordsUsedForValue == null ? new BitSet() : wordsUsedForValue;
+ }
+
+ /**
+ * The {@link MethodParameter} that was the target of the {@link ParameterResolver#resolve}
+ * operation.
+ */
+ public MethodParameter methodParameter() {
+ return methodParameter;
+ }
+
+ /**
+ * Represents the resolved value for the {@link MethodParameter} associated with this result.
+ */
+ public Object resolvedValue() {
+ return resolvedValue;
+ }
+
+ /**
+ * Represents the full set of words used to resolve the {@link MethodParameter}. This includes
+ * any tags/keys consumed from the input.
+ */
+ public BitSet wordsUsed() {
+ return wordsUsed;
+ }
+
+ /**
+ * Represents the full set of words used to resolve the value of this {@link MethodParameter}.
+ */
+ public BitSet wordsUsedForValue() {
+ return wordsUsedForValue;
+ }
+
+ public List wordsUsed(List words) {
+ return wordsUsed.stream().mapToObj(index -> words.get(index)).collect(Collectors.toList());
+ }
+
+ public List wordsUsedForValue(List words) {
+ return wordsUsedForValue.stream().mapToObj(index -> words.get(index)).collect(Collectors.toList());
+ }
+
+}
diff --git a/spring-shell2-core/src/test/java/org/springframework/shell2/ShellTest.java b/spring-shell2-core/src/test/java/org/springframework/shell2/ShellTest.java
index e1bdc3a0..8cac4511 100644
--- a/spring-shell2-core/src/test/java/org/springframework/shell2/ShellTest.java
+++ b/spring-shell2-core/src/test/java/org/springframework/shell2/ShellTest.java
@@ -52,6 +52,8 @@ public class ShellTest {
@Mock
private ParameterResolver parameterResolver;
+
+ private ValueResult valueResult;
@InjectMocks
private Shell shell;
@@ -67,6 +69,8 @@ public class ShellTest {
public void commandMatch() throws IOException {
when(parameterResolver.supports(any())).thenReturn(true);
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
+ valueResult = new ValueResult(null, "test");
+ when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
doThrow(new Exit()).when(resultHandler).handleResult(any());
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, "Say hello"));
@@ -102,6 +106,8 @@ public class ShellTest {
public void noCommand() throws IOException {
when(parameterResolver.supports(any())).thenReturn(true);
when(inputProvider.readInput()).thenReturn(() -> "", () -> "hello world how are you doing ?");
+ valueResult = new ValueResult(null, "test");
+ when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
doThrow(new Exit()).when(resultHandler).handleResult(any());
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, "Say hello"));
diff --git a/spring-shell2-jcommander-adapter/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java b/spring-shell2-jcommander-adapter/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java
index 0afa6813..fb1d0c5b 100644
--- a/spring-shell2-jcommander-adapter/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java
+++ b/spring-shell2-jcommander-adapter/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java
@@ -19,7 +19,6 @@ package org.springframework.shell2.jcommander;
import static org.springframework.shell2.Utils.unCamelify;
import java.lang.annotation.Annotation;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -28,22 +27,22 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import com.beust.jcommander.DynamicParameter;
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.ParametersDelegate;
-
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.shell2.CompletionContext;
import org.springframework.shell2.CompletionProposal;
import org.springframework.shell2.ParameterDescription;
import org.springframework.shell2.ParameterResolver;
-import org.springframework.shell2.Utils;
+import org.springframework.shell2.ValueResult;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
+import com.beust.jcommander.DynamicParameter;
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.ParametersDelegate;
+
/**
* Provides integration with JCommander.
*
@@ -80,10 +79,10 @@ public class JCommanderParameterResolver implements ParameterResolver {
}
@Override
- public Object resolve(MethodParameter methodParameter, List words) {
+ public ValueResult resolve(MethodParameter methodParameter, List words) {
JCommander jCommander = createJCommander(methodParameter);
jCommander.parse(words.toArray(new String[words.size()]));
- return jCommander.getObjects().get(0);
+ return new ValueResult(methodParameter, jCommander.getObjects().get(0));
}
private JCommander createJCommander(MethodParameter methodParameter) {
diff --git a/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java b/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java
index 09195055..1201b90f 100644
--- a/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java
+++ b/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java
@@ -63,7 +63,9 @@ public class JCommanderParameterResolverTest {
public void testPojoValuesAreCorrectlySet() {
MethodParameter methodParameter = Utils.createMethodParameter(COMMAND_METHOD, 0);
- FieldCollins resolved = (FieldCollins) resolver.resolve(methodParameter, asList("--name foo -level 2 something-else yet-something-else".split(" ")));
+ FieldCollins resolved = (FieldCollins) resolver
+ .resolve(methodParameter, asList("--name foo -level 2 something-else yet-something-else".split(" ")))
+ .resolvedValue();
assertThat(resolved.getName()).isEqualTo("foo");
assertThat(resolved.getLevel()).isEqualTo(2);
diff --git a/spring-shell2-shell1-adapter/pom.xml b/spring-shell2-shell1-adapter/pom.xml
index 6eb83809..80ea95ce 100644
--- a/spring-shell2-shell1-adapter/pom.xml
+++ b/spring-shell2-shell1-adapter/pom.xml
@@ -27,6 +27,11 @@
org.springframework.boot
spring-boot-starter-test
+
+ org.springframework.shell
+ spring-shell2-core-tests
+ test
+
org.assertj
assertj-core
diff --git a/spring-shell2-shell1-adapter/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java b/spring-shell2-shell1-adapter/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java
index c3da1053..4f3a2942 100644
--- a/spring-shell2-shell1-adapter/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java
+++ b/spring-shell2-shell1-adapter/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java
@@ -19,6 +19,7 @@ package org.springframework.shell2.legacy;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@@ -36,6 +37,7 @@ import org.springframework.shell2.CompletionContext;
import org.springframework.shell2.CompletionProposal;
import org.springframework.shell2.ParameterDescription;
import org.springframework.shell2.ParameterResolver;
+import org.springframework.shell2.ValueResult;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@@ -64,21 +66,23 @@ public class LegacyParameterResolver implements ParameterResolver {
}
@Override
- public Object resolve(MethodParameter methodParameter, List words) {
+ public ValueResult resolve(MethodParameter methodParameter, List words) {
CliOption cliOption = methodParameter.getParameterAnnotation(CliOption.class);
Optional> converter = converters.stream()
.filter(c -> c.supports(methodParameter.getParameterType(), cliOption.optionContext()))
.findFirst();
- Map values = parseOptions(words);
- Map seenValues = convertValues(values, methodParameter, converter);
+ Map values = parseOptions(words);
+ Map seenValues = convertValues(values, methodParameter, converter);
switch (seenValues.size()) {
case 0:
if (!cliOption.mandatory()) {
String value = cliOption.unspecifiedDefaultValue();
- return converter
+ Object resolvedValue = converter
.orElseThrow(noConverterFound(cliOption.key()[0], value, methodParameter.getParameterType()))
.convertFromText(value, methodParameter.getParameterType(), cliOption.optionContext());
+
+ return new ValueResult(methodParameter, resolvedValue);
}
else {
throw new IllegalArgumentException("Could not find parameter values for " + prettifyKeys(Arrays.asList(cliOption.key())) + " in " + words);
@@ -117,38 +121,49 @@ public class LegacyParameterResolver implements ParameterResolver {
return null;
}
- private Map parseOptions(List words) {
- Map values = new HashMap<>();
+ private Map parseOptions(List words) {
+ Map values = new HashMap<>();
for (int i = 0; i < words.size(); i++) {
+ int from = i;
String word = words.get(i);
- if (word.startsWith("--")) {
- String key = word.substring("--".length());
+ if (word.startsWith(CLI_PREFIX)) {
+ String key = word.substring(CLI_PREFIX.length());
// If next word doesn't exist or starts with '--', this is an unary option. Store null
- String value = i < words.size() - 1 && !words.get(i + 1).startsWith("--") ? words.get(++i) : null;
- Assert.isTrue(!values.containsKey(key), String.format("Option --%s has already been set", key));
- values.put(key, value);
+ String value = i < words.size() - 1 && !words.get(i + 1).startsWith(CLI_PREFIX) ? words.get(++i) : null;
+ Assert.isTrue(!values.containsKey(key), String.format("Option %s%s has already been set", CLI_PREFIX, key));
+ values.put(key, new ParseResult(value, from));
} // Must be the 'anonymous' option
else {
Assert.isTrue(!values.containsKey(""), "Anonymous option has already been set");
- values.put("", word);
+ values.put("", new ParseResult(word, from));
}
}
return values;
}
- private Map convertValues(Map values, MethodParameter methodParameter, Optional> converter) {
- Map seenValues = new HashMap<>();
+ private Map convertValues(Map values, MethodParameter methodParameter, Optional> converter) {
+ Map seenValues = new HashMap<>();
CliOption option = methodParameter.getParameterAnnotation(CliOption.class);
for (String key : option.key()) {
if (values.containsKey(key)) {
- String value = values.get(key);
+ ParseResult parseResult = values.get(key);
+ String value = parseResult.value;
if (value == null && !CLI_OPTION_NULL.equals(option.specifiedDefaultValue())) {
value = option.specifiedDefaultValue();
}
Class> parameterType = methodParameter.getParameterType();
- seenValues.put(key, converter
+ Object resolvedValue = converter
.orElseThrow(noConverterFound(key, value, parameterType))
- .convertFromText(value, parameterType, option.optionContext()));
+ .convertFromText(value, parameterType, option.optionContext());
+ int from = parseResult.from;
+ int to = key.isEmpty() || parseResult.value == null ? from : from + 1;
+ BitSet wordsUsed = new BitSet();
+ wordsUsed.set(from, to + 1);
+ BitSet wordsUsedForValues = new BitSet();
+ if (parseResult.value != null) {
+ wordsUsedForValues.set(to);
+ }
+ seenValues.put(key, new ValueResult(methodParameter, resolvedValue, wordsUsed, wordsUsedForValues));
}
}
return seenValues;
@@ -158,11 +173,23 @@ public class LegacyParameterResolver implements ParameterResolver {
* Return the list of possible keys for an option, suitable for displaying in an error message.
*/
private String prettifyKeys(Collection keys) {
- return keys.stream().map(s -> "".equals(s) ? "" : "--" + s).collect(Collectors.joining(", ", "[", "]"));
+ return keys.stream().map(s -> "".equals(s) ? "" : CLI_PREFIX + s).collect(Collectors.joining(", ", "[", "]"));
}
private Supplier noConverterFound(String key, String value, Class> parameterType) {
- return () -> new IllegalStateException("No converter found for --" + key + " from '" + value + "' to type " + parameterType);
+ return () -> new IllegalStateException("No converter found for " + CLI_PREFIX + key + " from '" + value + "' to type " + parameterType);
+ }
+
+ private static class ParseResult {
+ private final String value;
+
+ private final Integer from;
+
+ public ParseResult(String value, Integer from) {
+ this.value = value;
+ this.from = from;
+ }
+
}
}
diff --git a/spring-shell2-shell1-adapter/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java b/spring-shell2-shell1-adapter/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java
index 44cea25c..5ecbe21b 100644
--- a/spring-shell2-shell1-adapter/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java
+++ b/spring-shell2-shell1-adapter/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java
@@ -18,13 +18,13 @@ package org.springframework.shell2.legacy;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.shell2.ValueResultAsserts.assertThat;
import static org.springframework.shell2.legacy.LegacyCommands.REGISTER_METHOD;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
-
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -37,6 +37,7 @@ import org.springframework.shell.core.annotation.CliOption;
import org.springframework.shell2.ParameterDescription;
import org.springframework.shell2.ParameterResolver;
import org.springframework.shell2.Utils;
+import org.springframework.shell2.ValueResult;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -68,47 +69,46 @@ public class LegacyParameterResolverTest {
public void resolvesParameterAnnotatedWithCliOption() throws Exception {
MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, NAME_OR_ANONYMOUS);
- Object result = resolve(methodParameter, "--foo bar --name baz --qix bux");
-
- assertThat(result).isEqualTo("baz");
+ assertThat(resolve(methodParameter, "--foo bar --name baz --qix bux")).hasValue("baz").usesWords(2, 3)
+ .usesWordsForValue(3);
}
@Test
public void resolvesAnonymousParameterAnnotatedWithCliOption() throws Exception {
MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, NAME_OR_ANONYMOUS);
- Object result = resolve(methodParameter, "--foo bar baz --qix bux");
- assertThat(result).isEqualTo("baz");
+ assertThat(resolve(methodParameter, "--foo bar baz --qix bux")).hasValue("baz").usesWords(2)
+ .usesWordsForValue(2);
- // As first param
- result = resolve(methodParameter, "baz --foo bar --qix bux");
- assertThat(result).isEqualTo("baz");
+ assertThat(resolve(methodParameter, "baz --foo bar --qix bux")).hasValue("baz").usesWords(0)
+ .usesWordsForValue(0);
}
@Test
public void usesLegacyConverters() throws Exception {
MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, TYPE);
- Object result = resolve(methodParameter, "--foo bar --name baz --qix bux --type processor");
-
- assertThat(result).isSameAs(ArtifactType.processor);
+ assertThat(resolve(methodParameter, "--foo bar --name baz --qix bux --type processor"))
+ .hasValue(ArtifactType.processor).usesWords(6, 7).usesWordsForValue(7);
}
@Test
public void testUnspecifiedDefaultValue() throws Exception {
MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, FORCE);
- Object result = resolve(methodParameter, "--foo bar --name baz --qix bux");
-
- assertThat(result).isEqualTo(false);
+ assertThat(resolve(methodParameter, "--foo bar --name baz --qix bux"))
+ .hasValue(false).notUsesWords().notUsesWordsForValue();
}
@Test
public void testSpecifiedDefaultValue() throws Exception {
MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, FORCE);
- assertThat(resolve(methodParameter, "--force --foo bar --name baz --qix bux")).isEqualTo(true);
- assertThat(resolve(methodParameter, "--foo bar --name baz --qix bux --force")).isEqualTo(true);
+ assertThat(resolve(methodParameter, "--force --foo bar --name baz --qix bux"))
+ .hasValue(true).usesWords(0).notUsesWordsForValue();
+
+ assertThat(resolve(methodParameter, "--foo bar --name baz --qix bux --force"))
+ .hasValue(true).usesWords(6).notUsesWordsForValue();
}
@Test
@@ -116,7 +116,8 @@ public class LegacyParameterResolverTest {
MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, COORDINATES);
thrown.expect(IllegalArgumentException.class);
- thrown.expectMessage("Could not find parameter values for [--coordinates, --coords] in [--force, --foo, bar, --name, baz, --qix, bux]");
+ thrown.expectMessage(
+ "Could not find parameter values for [--coordinates, --coords] in [--force, --foo, bar, --name, baz, --qix, bux]");
resolve(methodParameter, "--force --foo bar --name baz --qix bux");
}
@@ -155,101 +156,102 @@ public class LegacyParameterResolverTest {
thrown.expectMessage("No converter found for --v2 from '42' to type int");
resolve(methodParameter, "--v1 1 --v2");
}
-
+
@Test
public void testDescribeBothDefaultsNotDeclared() {
MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.REGISTER_METHOD, 1);
-
+
ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
-
+
assertThat(description.keys()).containsExactly("--type");
assertThat(description.formal()).isEqualTo(Utils.unCamelify(ArtifactType.class.getSimpleName()));
assertThat(description.defaultValue().isPresent()).isFalse();
assertThat(description.mandatoryKey()).isTrue();
-
+
String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
assertThat(description.help()).isEqualTo(expectedHelp);
}
-
+
@Test
public void testDescribeBothDefaultsDeclared() {
MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SOME_METHOD, 1);
-
+
ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
-
+
assertThat(description.keys()).containsExactly("--option");
assertThat(description.formal()).isEqualTo(boolean.class.getName());
assertThat(description.defaultValue().get()).isEqualTo("false");
assertThat(description.defaultValueWhenFlag().get()).isEqualTo("true");
assertThat(description.mandatoryKey()).isTrue();
-
+
String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
assertThat(description.help()).isEqualTo(expectedHelp);
}
-
+
@Test
public void testDescribeOnlySpecifiedDefaultDeclared() {
MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SUM_METHOD, 1);
-
+
ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
-
+
assertThat(description.keys()).containsExactly("--v2");
assertThat(description.formal()).isEqualTo(int.class.getName());
assertThat(description.defaultValue().get()).isEqualTo("null");
assertThat(description.defaultValueWhenFlag().get()).isEqualTo("42");
assertThat(description.mandatoryKey()).isTrue();
-
- String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
- assertThat(description.help()).isEqualTo(expectedHelp);
- }
-
- @Test
- public void testDescribeOnlyUnspecifiedDefaultDeclared() {
- MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SUM_METHOD, 0);
-
- ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
-
- assertThat(description.keys()).containsExactly("--v1");
- assertThat(description.formal()).isEqualTo(int.class.getName());
- assertThat(description.defaultValue().get()).isEqualTo("38");
- assertThat(description.mandatoryKey()).isTrue();
-
- String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
- assertThat(description.help()).isEqualTo(expectedHelp);
- }
-
- @Test
- public void testDescribeDefaultKey() {
- MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.LEGACY_ECHO_METHOD, 0);
-
- ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
-
- assertThat(description.keys()).isEmpty();
- assertThat(description.formal()).isEqualTo(Utils.unCamelify(String.class.getSimpleName()));
- assertThat(description.defaultValue().isPresent()).isFalse();
- assertThat(description.mandatoryKey()).isFalse();
-
- String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
- assertThat(description.help()).isEqualTo(expectedHelp);
- }
-
- @Test
- public void testDescribeNonMandatoryNoDefaults() {
- MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SOME_METHOD, 0);
-
- ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
-
- assertThat(description.keys()).containsExactly("--key");
- assertThat(description.formal()).isEqualTo(Utils.unCamelify(String.class.getSimpleName()));
- assertThat(description.defaultValue().get()).isEqualTo("null");
- assertThat(description.mandatoryKey()).isTrue();
-
+
String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
assertThat(description.help()).isEqualTo(expectedHelp);
}
- private Object resolve(MethodParameter methodParameter, String command) {
- return parameterResolver.resolve(methodParameter, asList(command.split(" ")));
+ @Test
+ public void testDescribeOnlyUnspecifiedDefaultDeclared() {
+ MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SUM_METHOD, 0);
+
+ ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
+
+ assertThat(description.keys()).containsExactly("--v1");
+ assertThat(description.formal()).isEqualTo(int.class.getName());
+ assertThat(description.defaultValue().get()).isEqualTo("38");
+ assertThat(description.mandatoryKey()).isTrue();
+
+ String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
+ assertThat(description.help()).isEqualTo(expectedHelp);
+ }
+
+ @Test
+ public void testDescribeDefaultKey() {
+ MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.LEGACY_ECHO_METHOD, 0);
+
+ ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
+
+ assertThat(description.keys()).isEmpty();
+ assertThat(description.formal()).isEqualTo(Utils.unCamelify(String.class.getSimpleName()));
+ assertThat(description.defaultValue().isPresent()).isFalse();
+ assertThat(description.mandatoryKey()).isFalse();
+
+ String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
+ assertThat(description.help()).isEqualTo(expectedHelp);
+ }
+
+ @Test
+ public void testDescribeNonMandatoryNoDefaults() {
+ MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SOME_METHOD, 0);
+
+ ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get();
+
+ assertThat(description.keys()).containsExactly("--key");
+ assertThat(description.formal()).isEqualTo(Utils.unCamelify(String.class.getSimpleName()));
+ assertThat(description.defaultValue().get()).isEqualTo("null");
+ assertThat(description.mandatoryKey()).isTrue();
+
+ String expectedHelp = methodParameter.getParameterAnnotation(CliOption.class).help();
+ assertThat(description.help()).isEqualTo(expectedHelp);
+ }
+
+ private ValueResult resolve(MethodParameter methodParameter, String command) {
+ ValueResult result = parameterResolver.resolve(methodParameter, asList(command.split(" ")));
+ return result;
}
@Configuration
diff --git a/spring-shell2-standard/pom.xml b/spring-shell2-standard/pom.xml
index 3c381e95..6ea5b41f 100644
--- a/spring-shell2-standard/pom.xml
+++ b/spring-shell2-standard/pom.xml
@@ -17,16 +17,19 @@
org.springframework.shell
spring-shell2-core
-
org.springframework.boot
spring-boot-starter-test
+
+ org.springframework.shell
+ spring-shell2-core-tests
+ test
+
org.assertj
assertj-core
test
-
diff --git a/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java b/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java
index 13aba9f0..e9c29d20 100644
--- a/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java
+++ b/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 the original author or authors.
+ * Copyright 2015-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,14 @@
package org.springframework.shell2.standard;
+import static org.springframework.shell2.Utils.unCamelify;
+
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -44,13 +47,12 @@ import org.springframework.shell2.ParameterMissingResolutionException;
import org.springframework.shell2.ParameterResolver;
import org.springframework.shell2.UnfinishedParameterResolutionException;
import org.springframework.shell2.Utils;
+import org.springframework.shell2.ValueResult;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;
-import static org.springframework.shell2.Utils.unCamelify;
-
/**
* Default ParameterResolver implementation that supports the following features:
* - named parameters (recognized because they start with some {@link ShellMethod#prefix()})
@@ -73,6 +75,7 @@ import static org.springframework.shell2.Utils.unCamelify;
* if needed.
* @author Eric Bottard
* @author Florent Biville
+ * @author Camilo Gonzalez
*/
@Component
public class StandardParameterResolver implements ParameterResolver {
@@ -105,7 +108,7 @@ public class StandardParameterResolver implements ParameterResolver {
}
@Override
- public Object resolve(MethodParameter methodParameter, List words) {
+ public ValueResult resolve(MethodParameter methodParameter, List words) {
String prefix = prefixForMethod(methodParameter.getMethod());
CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), words);
@@ -113,12 +116,15 @@ public class StandardParameterResolver implements ParameterResolver {
Map result = new HashMap<>();
Map namedParameters = new HashMap<>();
- List positionalValues = new ArrayList<>();
+
+ // index of words that haven't yet been used to resolve parameter values
+ List unusedWords = new ArrayList<>();
Set possibleKeys = gatherAllPossibleKeys(methodParameter.getMethod());
// First, resolve all parameters passed by-name
for (int i = 0; i < words.size(); i++) {
+ int from = i;
String word = words.get(i);
if (possibleKeys.contains(word)) {
String key = word;
@@ -133,16 +139,17 @@ public class StandardParameterResolver implements ParameterResolver {
String raw = words.subList(i + 1, i + 1 + arity).stream().collect(Collectors.joining(","));
Assert.isTrue(!namedParameters.containsKey(key), String.format("Parameter for '%s' has already been specified", word));
namedParameters.put(key, raw);
- result.put(parameter, ParameterRawValue.explicit(raw, key));
- i += arity;
if (arity == 0) {
boolean defaultValue = booleanDefaultValue(parameter);
// Boolean parameter has been specified. Use the opposite of the default value
- result.put(parameter, ParameterRawValue.explicit(String.valueOf(!defaultValue), key));
+ result.put(parameter, ParameterRawValue.explicit(String.valueOf(!defaultValue), key, from, from));
+ } else {
+ i += arity;
+ result.put(parameter, ParameterRawValue.explicit(raw, key, from, i));
}
} // store for later processing of positional params
else {
- positionalValues.add(word);
+ unusedWords.add(i);
}
}
@@ -157,14 +164,18 @@ public class StandardParameterResolver implements ParameterResolver {
copy.retainAll(namedParameters.keySet());
if (copy.isEmpty()) { // Was not set via a key (including aliases), must be positional
int arity = getArity(parameter);
- if (arity > 0 && (offset + arity) <= positionalValues.size()) {
- String raw = positionalValues.subList(offset, offset + arity).stream().collect(Collectors.joining(","));
- result.put(parameter, ParameterRawValue.explicit(raw, null));
+ if (arity > 0 && (offset + arity) <= unusedWords.size()) {
+ String raw = unusedWords.subList(offset, offset + arity).stream()
+ .map(index -> words.get(index))
+ .collect(Collectors.joining(","));
+ int from = unusedWords.get(offset);
+ int to = from + arity - 1;
+ result.put(parameter, ParameterRawValue.explicit(raw, null, from, to));
offset += arity;
} // No more input. Try defaultValues
else {
Optional defaultValue = defaultValueFor(parameter);
- defaultValue.ifPresent(value -> result.put(parameter, ParameterRawValue.implicit(value, null)));
+ defaultValue.ifPresent(value -> result.put(parameter, ParameterRawValue.implicit(value, null, null, null)));
}
}
else if (copy.size() > 1) {
@@ -172,8 +183,10 @@ public class StandardParameterResolver implements ParameterResolver {
}
}
- Assert.isTrue(offset == positionalValues.size(), "Too many arguments: the following could not be mapped to parameters: "
- + positionalValues.subList(offset, positionalValues.size()).stream().collect(Collectors.joining(" ", "'", "'")));
+ Assert.isTrue(offset == unusedWords.size(),
+ "Too many arguments: the following could not be mapped to parameters: "
+ + unusedWords.subList(offset, unusedWords.size()).stream()
+ .map(index -> words.get(index)).collect(Collectors.joining(" ", "'", "'")));
return result;
});
@@ -182,7 +195,31 @@ public class StandardParameterResolver implements ParameterResolver {
throw new ParameterMissingResolutionException(describe(methodParameter).findFirst().get());
}
ParameterRawValue parameterRawValue = resolved.get(param);
- return convertRawValue(parameterRawValue, methodParameter);
+ Object value = convertRawValue(parameterRawValue, methodParameter);
+ BitSet wordsUsed = getWordsUsed(parameterRawValue);
+ BitSet wordsUsedForValue = getWordsUsedForValue(parameterRawValue);
+ return new ValueResult(methodParameter, value, wordsUsed, wordsUsedForValue);
+ }
+
+ private BitSet getWordsUsed(ParameterRawValue parameterRawValue) {
+ if (parameterRawValue.from != null) {
+ BitSet wordsUsed = new BitSet();
+ wordsUsed.set(parameterRawValue.from, parameterRawValue.to + 1);
+ return wordsUsed;
+ }
+ return null;
+ }
+
+ private BitSet getWordsUsedForValue(ParameterRawValue parameterRawValue) {
+ if (parameterRawValue.from != null) {
+ BitSet wordsUsedForValue = new BitSet();
+ wordsUsedForValue.set(parameterRawValue.from, parameterRawValue.to + 1);
+ if (parameterRawValue.key != null) {
+ wordsUsedForValue.clear(parameterRawValue.from);
+ }
+ return wordsUsedForValue;
+ }
+ return null;
}
private Object convertRawValue(ParameterRawValue parameterRawValue, MethodParameter methodParameter) {
@@ -438,9 +475,9 @@ public class StandardParameterResolver implements ParameterResolver {
private CompletionContext context;
- private int from;
+ private Integer from;
- private int to;
+ private Integer to;
private Integer keyIndex;
@@ -459,18 +496,20 @@ public class StandardParameterResolver implements ParameterResolver {
*/
private final String key;
- private ParameterRawValue(String value, boolean explicit, String key) {
+ private ParameterRawValue(String value, boolean explicit, String key, Integer from, Integer to) {
this.value = value;
this.explicit = explicit;
this.key = key;
+ this.from = from;
+ this.to = to;
}
- public static ParameterRawValue explicit(String value, String key) {
- return new ParameterRawValue(value, true, key);
+ public static ParameterRawValue explicit(String value, String key, Integer from, Integer to) {
+ return new ParameterRawValue(value, true, key, from, to);
}
- public static ParameterRawValue implicit(String value, String key) {
- return new ParameterRawValue(value, false, key);
+ public static ParameterRawValue implicit(String value, String key, Integer from, Integer to) {
+ return new ParameterRawValue(value, false, key, from, to);
}
public boolean positional() {
@@ -480,10 +519,12 @@ public class StandardParameterResolver implements ParameterResolver {
@Override
public String toString() {
return "ParameterRawValue{" +
- "value='" + value + '\'' +
- ", explicit=" + explicit +
- ", key='" + key + '\'' +
- '}';
+ "value='" + value + '\'' +
+ ", explicit=" + explicit +
+ ", key='" + key + '\'' +
+ ", from=" + from +
+ ", to=" + to +
+ '}';
}
}
diff --git a/spring-shell2-standard/src/test/java/org/springframework/shell2/standard/StandardParameterResolverTest.java b/spring-shell2-standard/src/test/java/org/springframework/shell2/standard/StandardParameterResolverTest.java
index c3e755e3..f50b9a45 100644
--- a/spring-shell2-standard/src/test/java/org/springframework/shell2/standard/StandardParameterResolverTest.java
+++ b/spring-shell2-standard/src/test/java/org/springframework/shell2/standard/StandardParameterResolverTest.java
@@ -16,6 +16,12 @@
package org.springframework.shell2.standard;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.shell2.ValueResultAsserts.assertThat;
+import static org.springframework.util.ReflectionUtils.findMethod;
+
import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
@@ -25,18 +31,13 @@ import org.jline.reader.impl.DefaultParser;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
-
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.shell2.CompletionContext;
import org.springframework.shell2.CompletionProposal;
import org.springframework.shell2.ParameterMissingResolutionException;
import org.springframework.shell2.UnfinishedParameterResolutionException;
import org.springframework.shell2.Utils;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.springframework.util.ReflectionUtils.findMethod;
+import org.springframework.shell2.ValueResult;
/**
* Unit tests for DefaultParameterResolver.
@@ -56,33 +57,31 @@ public class StandardParameterResolverTest {
public void testParses() throws Exception {
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
- assertThat(resolver.resolve(
- Utils.createMethodParameter(method, 0),
- asList("--force --name --foo y".split(" "))
- )).isEqualTo(true);
- assertThat(resolver.resolve(
- Utils.createMethodParameter(method, 1),
- asList("--force --name --foo y".split(" "))
- )).isEqualTo("--foo");
- assertThat(resolver.resolve(
- Utils.createMethodParameter(method, 2),
- asList("--force --name --foo y".split(" "))
- )).isEqualTo("y");
- assertThat(resolver.resolve(
- Utils.createMethodParameter(method, 3),
- asList("--force --name --foo y".split(" "))
- )).isEqualTo("last");
-
+ List words = asList("--force --name --foo y".split(" "));
+ ValueResult result0 = resolver.resolve(Utils.createMethodParameter(method, 0), words);
+ assertThat(result0).hasValue(true).usesWords(0).notUsesWordsForValue();
+ assertThat(result0.wordsUsed(words)).containsExactly("--force");
+
+ ValueResult result1 = resolver.resolve(Utils.createMethodParameter(method, 1), words);
+ assertThat(result1).hasValue("--foo").usesWords(1, 2).usesWordsForValue(2);
+ assertThat(result1.wordsUsed(words)).containsExactly("--name", "--foo");
+ assertThat(result1.wordsUsedForValue(words)).containsExactly("--foo");
+
+ ValueResult result2 = resolver.resolve(Utils.createMethodParameter(method, 2), words);
+ assertThat(result2).hasValue("y").usesWords(3).usesWordsForValue(3);
+ assertThat(result2.wordsUsed(words)).containsExactly("y");
+
+ ValueResult result3 = resolver.resolve(Utils.createMethodParameter(method, 3), words);
+ assertThat(result3).hasValue("last").notUsesWords().notUsesWordsForValue();
}
-
+
@Test
public void testParsesWithMethodPrefix() throws Exception {
Method method = findMethod(Remote.class, "prefixTest", String.class);
- assertThat(resolver.resolve(
- Utils.createMethodParameter(method, 0),
- asList("-message abc".split(" "))
- )).isEqualTo("abc");
+ ValueResult result = resolver.resolve(Utils.createMethodParameter(method, 0),
+ asList("-message abc".split(" ")));
+ assertThat(result).hasValue("abc").usesWords(0, 1).usesWordsForValue(1);
}
@Test
@@ -244,8 +243,6 @@ public class StandardParameterResolverTest {
}
}
-
-
private CompletionContext contextFor(String input) {
DefaultParser defaultParser = new DefaultParser();
ParsedLine parsed = defaultParser.parse(input, input.length());