Expose an evaluate() method that returns result before handling

Fixes #154
This commit is contained in:
Eric Bottard
2017-09-08 21:48:31 +02:00
parent a6f3e28f8f
commit 8fd4d69a61

View File

@@ -41,10 +41,14 @@ import org.springframework.util.ReflectionUtils;
/**
* Main class implementing a shell loop.
*
* <p>Given some textual input, locate the {@link MethodTarget} to invoke and {@link ResultHandler#handleResult(Object) handle}
* the result.</p>
* <p>
* Given some textual input, locate the {@link MethodTarget} to invoke and
* {@link ResultHandler#handleResult(Object) handle} the result.
* </p>
*
* <p>Also provides hooks for code completion</p>
* <p>
* Also provides hooks for code completion
* </p>
*
* @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<ParameterResolver> 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<ParameterResolver> 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.
* <p>
* This method has public visibility so that it can be invoked by actual commands
* (<em>e.g.</em> a {@literal script} command).
* </p>
*/
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.
*
* <p>This method has public visibility so that it can be invoked by actual commands
* (<em>e.g.</em> a {@literal script} command).</p>
* Evaluate a single "line" of input from the user by trying to map words to a command and
* arguments.
*
* <p>
* This method does not throw exceptions, it catches them and returns them as a regular
* result
* </p>
*/
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 (<em>e.g.</em> hitting ENTER on an empty line or blank space).
*
* <p>Also returns true (<em>i.e.</em> ask to ignore) when input starts with {@literal //}, which is used for comments.</p>
*/
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 (<em>e.g.</em> hitting ENTER on an
* empty line or blank space).
*
* <p>
* Also returns true (<em>i.e.</em> ask to ignore) when input starts with {@literal //},
* which is used for comments.
* </p>
*/
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<String> wordsForArguments(String command, List<String> 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<CompletionProposal> 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<MethodParameter> 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<CompletionProposal> 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<ConstraintViolation<Object>> 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<String> wordsForArgs) {
List<MethodParameter> 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;
}