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 a74f971b..6dcc12f4 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 @@ -17,6 +17,7 @@ package org.springframework.shell2; import java.util.List; +import java.util.stream.Stream; import org.springframework.core.MethodParameter; @@ -39,8 +40,10 @@ public interface ParameterResolver { /** * Describe a supported parameter, so that integrated help can be generated. + *

Typical implementations will return a one element stream result, but some may return several (for + * example if binding several words to a POJO).

*/ - ParameterDescription describe(MethodParameter parameter); + Stream describe(MethodParameter parameter); /** * Invoked during TAB completion. If the {@link CompletionContext} can be interpreted as the start 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 a4b857d6..fe5cb4ec 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 @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; import com.beust.jcommander.DynamicParameter; import com.beust.jcommander.JCommander; @@ -33,6 +34,7 @@ import org.springframework.shell2.CompletionContext; import org.springframework.shell2.CompletionProposal; import org.springframework.shell2.ParameterDescription; import org.springframework.shell2.ParameterResolver; +import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; /** @@ -40,6 +42,7 @@ import org.springframework.util.ReflectionUtils; * * @author Eric Bottard */ +@Component public class JCommanderParameterResolver implements ParameterResolver { private static final Collection> JCOMMANDER_ANNOTATIONS = @@ -80,7 +83,7 @@ public class JCommanderParameterResolver implements ParameterResolver { } @Override - public ParameterDescription describe(MethodParameter parameter) { + public Stream describe(MethodParameter parameter) { throw new UnsupportedOperationException(); } diff --git a/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java b/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java index 9a50492f..a8ef3dae 100644 --- a/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java +++ b/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java @@ -22,7 +22,10 @@ import java.util.List; import com.beust.jcommander.Parameter; /** - * Created by ericbottard on 15/12/15. + * A POJO with fields annotated with JCommander annotations. + * + * @author Eric Bottard + * @see MyLordCommands#genesis(FieldCollins) */ public class FieldCollins { 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 37be2556..0428ffa1 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 @@ -28,7 +28,10 @@ import org.springframework.shell2.Utils; import org.springframework.util.ReflectionUtils; /** - * Created by ericbottard on 15/12/15. + * Unit test for {@link JCommanderParameterResolver}. + * + * @author Eric Bottard + * @author Florent Biville */ public class JCommanderParameterResolverTest { diff --git a/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java b/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java index 236af2d5..0ef47ae6 100644 --- a/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java +++ b/spring-shell2-jcommander-adapter/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java @@ -17,14 +17,22 @@ package org.springframework.shell2.jcommander; /** - * Created by ericbottard on 15/12/15. + * An hypothetical command class, with one method using JCommander args, and the other not. + * + * @author Eric Bottard */ public class MyLordCommands { + /** + * This method should be supported. + */ public void genesis(FieldCollins fieldCollins) { } + /** + * This method is not. + */ public void apocalypse(String param) { } diff --git a/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/jcommander/Args.java b/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/jcommander/Args.java new file mode 100644 index 00000000..32c0309e --- /dev/null +++ b/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/jcommander/Args.java @@ -0,0 +1,52 @@ +/* + * 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.samples.jcommander; + +import java.util.ArrayList; +import java.util.List; + +import com.beust.jcommander.Parameter; + +/** + * An example straight from the JCommander documentation. + * + * @author Eric Bottard + * @author Cédric Beust + */ +public class Args { + @Parameter + private List parameters = new ArrayList<>(); + + @Parameter(names = { "-log", "-verbose" }, description = "Level of verbosity") + private Integer verbose = 1; + + @Parameter(names = "-groups", description = "Comma-separated list of group names to be run") + private String groups; + + @Parameter(names = "-debug", description = "Debug mode") + private boolean debug = false; + + @Override + public String toString() { + return "Args{" + + "parameters=" + parameters + + ", verbose=" + verbose + + ", groups='" + groups + '\'' + + ", debug=" + debug + + '}'; + } +} diff --git a/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/jcommander/JCommanderCommands.java b/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/jcommander/JCommanderCommands.java new file mode 100644 index 00000000..83c1334d --- /dev/null +++ b/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/jcommander/JCommanderCommands.java @@ -0,0 +1,35 @@ +/* + * 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.samples.jcommander; + +import org.springframework.shell2.standard.ShellComponent; +import org.springframework.shell2.standard.ShellMethod; +import org.springframework.shell2.standard.ShellOption; + +/** + * A class with JCommander commands. + * + * @author Eric Bottard + */ +@ShellComponent +public class JCommanderCommands { + + @ShellMethod(help = "bind parameters to JCommander POJO") + public String jcommander(@ShellOption(optOut = true) Args args) { + return "You said " + args; + } +} 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 17a1ac6e..c3da1053 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 @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; @@ -90,7 +91,7 @@ public class LegacyParameterResolver implements ParameterResolver { } @Override - public ParameterDescription describe(MethodParameter parameter) { + public Stream describe(MethodParameter parameter) { Parameter jlrParameter = parameter.getMethod().getParameters()[parameter.getParameterIndex()]; CliOption option = jlrParameter.getAnnotation(CliOption.class); ParameterDescription result = ParameterDescription.outOf(parameter); @@ -108,7 +109,7 @@ public class LegacyParameterResolver implements ParameterResolver { } boolean containsEmptyKey = keys.contains(""); result.mandatoryKey(!containsEmptyKey); - return result; + return Stream.of(result); } @Override 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 50a83ea3..44cea25c 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 @@ -160,7 +160,7 @@ public class LegacyParameterResolverTest { public void testDescribeBothDefaultsNotDeclared() { MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.REGISTER_METHOD, 1); - ParameterDescription description = parameterResolver.describe(methodParameter); + ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get(); assertThat(description.keys()).containsExactly("--type"); assertThat(description.formal()).isEqualTo(Utils.unCamelify(ArtifactType.class.getSimpleName())); @@ -175,7 +175,7 @@ public class LegacyParameterResolverTest { public void testDescribeBothDefaultsDeclared() { MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SOME_METHOD, 1); - ParameterDescription description = parameterResolver.describe(methodParameter); + ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get(); assertThat(description.keys()).containsExactly("--option"); assertThat(description.formal()).isEqualTo(boolean.class.getName()); @@ -191,7 +191,7 @@ public class LegacyParameterResolverTest { public void testDescribeOnlySpecifiedDefaultDeclared() { MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SUM_METHOD, 1); - ParameterDescription description = parameterResolver.describe(methodParameter); + ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get(); assertThat(description.keys()).containsExactly("--v2"); assertThat(description.formal()).isEqualTo(int.class.getName()); @@ -207,7 +207,7 @@ public class LegacyParameterResolverTest { public void testDescribeOnlyUnspecifiedDefaultDeclared() { MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SUM_METHOD, 0); - ParameterDescription description = parameterResolver.describe(methodParameter); + ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get(); assertThat(description.keys()).containsExactly("--v1"); assertThat(description.formal()).isEqualTo(int.class.getName()); @@ -222,7 +222,7 @@ public class LegacyParameterResolverTest { public void testDescribeDefaultKey() { MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.LEGACY_ECHO_METHOD, 0); - ParameterDescription description = parameterResolver.describe(methodParameter); + ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get(); assertThat(description.keys()).isEmpty(); assertThat(description.formal()).isEqualTo(Utils.unCamelify(String.class.getSimpleName())); @@ -237,7 +237,7 @@ public class LegacyParameterResolverTest { public void testDescribeNonMandatoryNoDefaults() { MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SOME_METHOD, 0); - ParameterDescription description = parameterResolver.describe(methodParameter); + ParameterDescription description = parameterResolver.describe(methodParameter).findFirst().get(); assertThat(description.keys()).containsExactly("--key"); assertThat(description.formal()).isEqualTo(Utils.unCamelify(String.class.getSimpleName())); diff --git a/spring-shell2-standard-commands/src/main/java/org/springframework/shell2/standard/commands/Help.java b/spring-shell2-standard-commands/src/main/java/org/springframework/shell2/standard/commands/Help.java index 3615ae19..48bdc156 100644 --- a/spring-shell2-standard-commands/src/main/java/org/springframework/shell2/standard/commands/Help.java +++ b/spring-shell2-standard-commands/src/main/java/org/springframework/shell2/standard/commands/Help.java @@ -244,7 +244,7 @@ public class Help { for (ParameterResolver parameterResolver : parameterResolvers) { MethodParameter methodParameter = Utils.createMethodParameter(methodTarget.getMethod(), i); if (parameterResolver.supports(methodParameter)) { - parameterDescriptions.add(parameterResolver.describe(methodParameter)); + parameterDescriptions.add(parameterResolver.describe(methodParameter).findFirst().get()); break; } } diff --git a/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/ShellOption.java b/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/ShellOption.java index 6978feeb..880d32a1 100644 --- a/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/ShellOption.java +++ b/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/ShellOption.java @@ -66,6 +66,14 @@ public @interface ShellOption { Class valueProvider() default NoValueProvider.class; + /** + * Used to indicate to the framework that the given parameter should NOT be resolved by + * {@link StandardParameterResolver}. This is useful if several implementations of + * {@link org.springframework.shell2.ParameterResolver} are present, given that the standard one can work with no + * annotation at all. + */ + boolean optOut() default false; + interface NoValueProvider extends ValueProvider { } 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 9a2e9d19..13aba9f0 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 @@ -100,7 +100,8 @@ public class StandardParameterResolver implements ParameterResolver { @Override public boolean supports(MethodParameter parameter) { - return parameter.getMethodAnnotation(ShellMethod.class) != null; + boolean optOut = parameter.hasParameterAnnotation(ShellOption.class) && parameter.getParameterAnnotation(ShellOption.class).optOut(); + return !optOut && parameter.getMethodAnnotation(ShellMethod.class) != null; } @Override @@ -126,7 +127,7 @@ public class StandardParameterResolver implements ParameterResolver { if (i + 1 + arity > words.size()) { String input = words.subList(i, words.size()).stream().collect(Collectors.joining(" ")); - throw new UnfinishedParameterResolutionException(describe(Utils.createMethodParameter(parameter)), input); + throw new UnfinishedParameterResolutionException(describe(Utils.createMethodParameter(parameter)).findFirst().get(), input); } Assert.isTrue(i + 1 + arity <= words.size(), String.format("Not enough input for parameter '%s'", word)); String raw = words.subList(i + 1, i + 1 + arity).stream().collect(Collectors.joining(",")); @@ -178,7 +179,7 @@ public class StandardParameterResolver implements ParameterResolver { Parameter param = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()]; if (!resolved.containsKey(param)) { - throw new ParameterMissingResolutionException(describe(methodParameter)); + throw new ParameterMissingResolutionException(describe(methodParameter).findFirst().get()); } ParameterRawValue parameterRawValue = resolved.get(param); return convertRawValue(parameterRawValue, methodParameter); @@ -233,7 +234,7 @@ public class StandardParameterResolver implements ParameterResolver { } @Override - public ParameterDescription describe(MethodParameter parameter) { + public Stream describe(MethodParameter parameter) { Parameter jlrParameter = parameter.getMethod().getParameters()[parameter.getParameterIndex()]; int arity = getArity(jlrParameter); Class type = parameter.getParameterType(); @@ -259,7 +260,7 @@ public class StandardParameterResolver implements ParameterResolver { .collect(Collectors.toList())) .mandatoryKey(false); - return result; + return Stream.of(result); } @Override @@ -332,7 +333,7 @@ public class StandardParameterResolver implements ParameterResolver { private List argumentKeysThatStartWithContextPrefix(MethodParameter methodParameter, CompletionContext context) { String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : ""; - return describe(methodParameter).keys().stream() + return describe(methodParameter).flatMap(pd -> pd.keys().stream()) .filter(k -> k.startsWith(prefix)) .map(CompletionProposal::new) .collect(Collectors.toList());