diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java index 7611b12a..d4b32c5f 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java @@ -41,10 +41,14 @@ import org.springframework.util.ReflectionUtils; /** * Main class implementing a shell loop. * - *

Given some textual input, locate the {@link MethodTarget} to invoke and {@link ResultHandler#handleResult(Object) handle} - * the result.

+ *

+ * Given some textual input, locate the {@link MethodTarget} to invoke and + * {@link ResultHandler#handleResult(Object) handle} the result. + *

* - *

Also provides hooks for code completion

+ *

+ * Also provides hooks for code completion + *

* * @author Eric Bottard */ @@ -52,6 +56,11 @@ public class Shell implements CommandRegistry { private final ResultHandler resultHandler; + /** + * Marker object returned to signify that there was no input to turn into a command execution. + */ + public static final Object NO_INPUT = new Object(); + @Autowired protected ApplicationContext applicationContext; @@ -63,7 +72,8 @@ public class Shell implements CommandRegistry { protected List parameterResolvers; /** - * Marker object to distinguish unresolved arguments from {@code null}, which is a valid value. + * Marker object to distinguish unresolved arguments from {@code null}, which is a valid + * value. */ protected static final Object UNRESOLVED = new Object(); @@ -86,7 +96,7 @@ public class Shell implements CommandRegistry { methodTargets.values() .forEach(this::validateParameterResolvers); } - + @Autowired public void setParameterResolvers(List resolvers) { this.parameterResolvers = new ArrayList<>(resolvers); @@ -94,8 +104,12 @@ public class Shell implements CommandRegistry { } /** - * The main program loop: acquire input, try to match it to a command and evaluate. Repeat until a - * {@link ResultHandler} causes the process to exit or there is no input. + * The main program loop: acquire input, try to match it to a command and evaluate. Repeat + * until a {@link ResultHandler} causes the process to exit or there is no input. + *

+ * This method has public visibility so that it can be invoked by actual commands + * (e.g. a {@literal script} command). + *

*/ public void run(InputProvider inputProvider) throws IOException { while (true) { @@ -110,19 +124,25 @@ public class Shell implements CommandRegistry { if (input == null) { break; } - evaluate(input); + Object result = evaluate(input); + if (result != NO_INPUT) { + resultHandler.handleResult(result); + } } } /** - * Evaluate a single "line" of input from the user by trying to map words to a command and arguments. - * - *

This method has public visibility so that it can be invoked by actual commands - * (e.g. a {@literal script} command).

+ * Evaluate a single "line" of input from the user by trying to map words to a command and + * arguments. + * + *

+ * This method does not throw exceptions, it catches them and returns them as a regular + * result + *

*/ - public void evaluate(Input input) { + public Object evaluate(Input input) { if (noInput(input)) { - return; + return NO_INPUT; } String line = input.words().stream().collect(Collectors.joining(" ")).trim(); @@ -140,36 +160,40 @@ public class Shell implements CommandRegistry { try { Object[] args = resolveArgs(method, wordsForArgs); validateArgs(args, methodTarget); - result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args); + return ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args); } catch (Exception e) { - result = e; + return e; } - } else { - result = new CommandNotCurrentlyAvailable(command, availability); + } + else { + return new CommandNotCurrentlyAvailable(command, availability); } } else { - result = new CommandNotFound(words); + return new CommandNotFound(words); } - resultHandler.handleResult(result); - } - - /** - * Return true if the parsed input ends up being empty (e.g. hitting ENTER on an empty line or blank space). - * - *

Also returns true (i.e. ask to ignore) when input starts with {@literal //}, which is used for comments.

- */ - private boolean noInput(Input input) { - return input.words().isEmpty() - || (input.words().size() == 1 && input.words().get(0).trim().isEmpty()) - || (input.words().iterator().next().matches("\\s*//.*")); } /** - * Returns the list of words to be considered for argument resolving. Drops the first N words used for the - * command, as well as an optional empty word at the end of the list (which may be present if user added spaces - * before submitting the buffer) + * Return true if the parsed input ends up being empty (e.g. hitting ENTER on an + * empty line or blank space). + * + *

+ * Also returns true (i.e. ask to ignore) when input starts with {@literal //}, + * which is used for comments. + *

+ */ + private boolean noInput(Input input) { + return input.words().isEmpty() + || (input.words().size() == 1 && input.words().get(0).trim().isEmpty()) + || (input.words().iterator().next().matches("\\s*//.*")); + } + + /** + * Returns the list of words to be considered for argument resolving. Drops the first N + * words used for the command, as well as an optional empty word at the end of the list + * (which may be present if user added spaces before submitting the buffer) */ private List wordsForArguments(String command, List words) { int wordsUsedForCommandKey = command.split(" ").length; @@ -182,8 +206,9 @@ public class Shell implements CommandRegistry { } /** - * Gather completion proposals given some (incomplete) input the user has already typed in. - * When and how this method is invoked is implementation specific and decided by the actual user interface. + * Gather completion proposals given some (incomplete) input the user has already typed + * in. When and how this method is invoked is implementation specific and decided by the + * actual user interface. */ public List complete(CompletionContext context) { @@ -198,7 +223,7 @@ public class Shell implements CommandRegistry { // Try to complete arguments MethodTarget methodTarget = methodTargets.get(best); Method method = methodTarget.getMethod(); - + List parameters = Utils.createMethodParameters(method).collect(Collectors.toList()); for (ParameterResolver resolver : parameterResolvers) { for (int index = 0; index < parameters.size(); index++) { @@ -214,16 +239,16 @@ public class Shell implements CommandRegistry { private List commandsStartingWith(String prefix) { return methodTargets.entrySet().stream() - .filter(e -> e.getKey().startsWith(prefix)) - .map(e -> toCommandProposal(e.getKey(), e.getValue())) - .collect(Collectors.toList()); + .filter(e -> e.getKey().startsWith(prefix)) + .map(e -> toCommandProposal(e.getKey(), e.getValue())) + .collect(Collectors.toList()); } private CompletionProposal toCommandProposal(String command, MethodTarget methodTarget) { return new CompletionProposal(command) - .dontQuote(true) - .category("Available commands") - .description(methodTarget.getHelp()); + .dontQuote(true) + .category("Available commands") + .description(methodTarget.getHelp()); } private void validateArgs(Object[] args, MethodTarget methodTarget) { @@ -234,23 +259,22 @@ public class Shell implements CommandRegistry { } } Set> constraintViolations = validator.forExecutables().validateParameters( - methodTarget.getBean(), - methodTarget.getMethod(), - args - ); + methodTarget.getBean(), + methodTarget.getMethod(), + args); if (constraintViolations.size() > 0) { throw new ParameterValidationException(constraintViolations, methodTarget); } } /** - * Use all known {@link ParameterResolver}s to try to compute a value for each parameter of the method to - * invoke. - * @param method the method for which parameters should be computed + * Use all known {@link ParameterResolver}s to try to compute a value for each parameter + * of the method to invoke. + * @param method the method for which parameters should be computed * @param wordsForArgs the list of 'words' that should be converted to parameter values. - * May include markers for passing parameters 'by name' - * @return an array containing resolved parameter values, or {@link #UNRESOLVED} for parameters that could not be - * resolved + * May include markers for passing parameters 'by name' + * @return an array containing resolved parameter values, or {@link #UNRESOLVED} for + * parameters that could not be resolved */ private Object[] resolveArgs(Method method, List wordsForArgs) { List parameters = Utils.createMethodParameters(method).collect(Collectors.toList()); @@ -266,10 +290,10 @@ public class Shell implements CommandRegistry { } return args; } - + /** - * Verifies that we have at least one {@link ParameterResolver} that supports each of - * the {@link MethodParameter}s in the method. + * Verifies that we have at least one {@link ParameterResolver} that supports each of the + * {@link MethodParameter}s in the method. */ private void validateParameterResolvers(MethodTarget methodTarget) { Utils.createMethodParameters(methodTarget.getMethod()) @@ -280,7 +304,7 @@ public class Shell implements CommandRegistry { .orElseThrow(() -> new ParameterResolverMissingException(parameter)); }); } - + /** * Returns the longest command that can be matched as first word(s) in the given buffer. * @@ -288,8 +312,8 @@ public class Shell implements CommandRegistry { */ private String findLongestCommand(String prefix) { String result = methodTargets.keySet().stream() - .filter(command -> prefix.equals(command) || prefix.startsWith(command + " ")) - .reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2); + .filter(command -> prefix.equals(command) || prefix.startsWith(command + " ")) + .reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2); return "".equals(result) ? null : result; }