Expose an evaluate() method that returns result before handling
Fixes #154
This commit is contained in:
@@ -41,10 +41,14 @@ import org.springframework.util.ReflectionUtils;
|
|||||||
/**
|
/**
|
||||||
* Main class implementing a shell loop.
|
* Main class implementing a shell loop.
|
||||||
*
|
*
|
||||||
* <p>Given some textual input, locate the {@link MethodTarget} to invoke and {@link ResultHandler#handleResult(Object) handle}
|
* <p>
|
||||||
* the result.</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
|
* @author Eric Bottard
|
||||||
*/
|
*/
|
||||||
@@ -52,6 +56,11 @@ public class Shell implements CommandRegistry {
|
|||||||
|
|
||||||
private final ResultHandler resultHandler;
|
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
|
@Autowired
|
||||||
protected ApplicationContext applicationContext;
|
protected ApplicationContext applicationContext;
|
||||||
|
|
||||||
@@ -63,7 +72,8 @@ public class Shell implements CommandRegistry {
|
|||||||
protected List<ParameterResolver> parameterResolvers;
|
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();
|
protected static final Object UNRESOLVED = new Object();
|
||||||
|
|
||||||
@@ -86,7 +96,7 @@ public class Shell implements CommandRegistry {
|
|||||||
methodTargets.values()
|
methodTargets.values()
|
||||||
.forEach(this::validateParameterResolvers);
|
.forEach(this::validateParameterResolvers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void setParameterResolvers(List<ParameterResolver> resolvers) {
|
public void setParameterResolvers(List<ParameterResolver> resolvers) {
|
||||||
this.parameterResolvers = new ArrayList<>(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
|
* The main program loop: acquire input, try to match it to a command and evaluate. Repeat
|
||||||
* {@link ResultHandler} causes the process to exit or there is no input.
|
* 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 {
|
public void run(InputProvider inputProvider) throws IOException {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -110,19 +124,25 @@ public class Shell implements CommandRegistry {
|
|||||||
if (input == null) {
|
if (input == null) {
|
||||||
break;
|
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.
|
* 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>
|
* <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)) {
|
if (noInput(input)) {
|
||||||
return;
|
return NO_INPUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
String line = input.words().stream().collect(Collectors.joining(" ")).trim();
|
String line = input.words().stream().collect(Collectors.joining(" ")).trim();
|
||||||
@@ -140,36 +160,40 @@ public class Shell implements CommandRegistry {
|
|||||||
try {
|
try {
|
||||||
Object[] args = resolveArgs(method, wordsForArgs);
|
Object[] args = resolveArgs(method, wordsForArgs);
|
||||||
validateArgs(args, methodTarget);
|
validateArgs(args, methodTarget);
|
||||||
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
return ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
result = e;
|
return e;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
result = new CommandNotCurrentlyAvailable(command, availability);
|
else {
|
||||||
|
return new CommandNotCurrentlyAvailable(command, availability);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
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
|
* Return true if the parsed input ends up being empty (<em>e.g.</em> hitting ENTER on an
|
||||||
* command, as well as an optional empty word at the end of the list (which may be present if user added spaces
|
* empty line or blank space).
|
||||||
* before submitting the buffer)
|
*
|
||||||
|
* <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) {
|
private List<String> wordsForArguments(String command, List<String> words) {
|
||||||
int wordsUsedForCommandKey = command.split(" ").length;
|
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.
|
* Gather completion proposals given some (incomplete) input the user has already typed
|
||||||
* When and how this method is invoked is implementation specific and decided by the actual user interface.
|
* in. When and how this method is invoked is implementation specific and decided by the
|
||||||
|
* actual user interface.
|
||||||
*/
|
*/
|
||||||
public List<CompletionProposal> complete(CompletionContext context) {
|
public List<CompletionProposal> complete(CompletionContext context) {
|
||||||
|
|
||||||
@@ -198,7 +223,7 @@ public class Shell implements CommandRegistry {
|
|||||||
// Try to complete arguments
|
// Try to complete arguments
|
||||||
MethodTarget methodTarget = methodTargets.get(best);
|
MethodTarget methodTarget = methodTargets.get(best);
|
||||||
Method method = methodTarget.getMethod();
|
Method method = methodTarget.getMethod();
|
||||||
|
|
||||||
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
|
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
|
||||||
for (ParameterResolver resolver : parameterResolvers) {
|
for (ParameterResolver resolver : parameterResolvers) {
|
||||||
for (int index = 0; index < parameters.size(); index++) {
|
for (int index = 0; index < parameters.size(); index++) {
|
||||||
@@ -214,16 +239,16 @@ public class Shell implements CommandRegistry {
|
|||||||
|
|
||||||
private List<CompletionProposal> commandsStartingWith(String prefix) {
|
private List<CompletionProposal> commandsStartingWith(String prefix) {
|
||||||
return methodTargets.entrySet().stream()
|
return methodTargets.entrySet().stream()
|
||||||
.filter(e -> e.getKey().startsWith(prefix))
|
.filter(e -> e.getKey().startsWith(prefix))
|
||||||
.map(e -> toCommandProposal(e.getKey(), e.getValue()))
|
.map(e -> toCommandProposal(e.getKey(), e.getValue()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletionProposal toCommandProposal(String command, MethodTarget methodTarget) {
|
private CompletionProposal toCommandProposal(String command, MethodTarget methodTarget) {
|
||||||
return new CompletionProposal(command)
|
return new CompletionProposal(command)
|
||||||
.dontQuote(true)
|
.dontQuote(true)
|
||||||
.category("Available commands")
|
.category("Available commands")
|
||||||
.description(methodTarget.getHelp());
|
.description(methodTarget.getHelp());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateArgs(Object[] args, MethodTarget methodTarget) {
|
private void validateArgs(Object[] args, MethodTarget methodTarget) {
|
||||||
@@ -234,23 +259,22 @@ public class Shell implements CommandRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables().validateParameters(
|
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables().validateParameters(
|
||||||
methodTarget.getBean(),
|
methodTarget.getBean(),
|
||||||
methodTarget.getMethod(),
|
methodTarget.getMethod(),
|
||||||
args
|
args);
|
||||||
);
|
|
||||||
if (constraintViolations.size() > 0) {
|
if (constraintViolations.size() > 0) {
|
||||||
throw new ParameterValidationException(constraintViolations, methodTarget);
|
throw new ParameterValidationException(constraintViolations, methodTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use all known {@link ParameterResolver}s to try to compute a value for each parameter of the method to
|
* Use all known {@link ParameterResolver}s to try to compute a value for each parameter
|
||||||
* invoke.
|
* of the method to invoke.
|
||||||
* @param method the method for which parameters should be computed
|
* @param method the method for which parameters should be computed
|
||||||
* @param wordsForArgs the list of 'words' that should be converted to parameter values.
|
* @param wordsForArgs the list of 'words' that should be converted to parameter values.
|
||||||
* May include markers for passing parameters 'by name'
|
* May include markers for passing parameters 'by name'
|
||||||
* @return an array containing resolved parameter values, or {@link #UNRESOLVED} for parameters that could not be
|
* @return an array containing resolved parameter values, or {@link #UNRESOLVED} for
|
||||||
* resolved
|
* parameters that could not be resolved
|
||||||
*/
|
*/
|
||||||
private Object[] resolveArgs(Method method, List<String> wordsForArgs) {
|
private Object[] resolveArgs(Method method, List<String> wordsForArgs) {
|
||||||
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
|
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
|
||||||
@@ -266,10 +290,10 @@ public class Shell implements CommandRegistry {
|
|||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that we have at least one {@link ParameterResolver} that supports each of
|
* Verifies that we have at least one {@link ParameterResolver} that supports each of the
|
||||||
* the {@link MethodParameter}s in the method.
|
* {@link MethodParameter}s in the method.
|
||||||
*/
|
*/
|
||||||
private void validateParameterResolvers(MethodTarget methodTarget) {
|
private void validateParameterResolvers(MethodTarget methodTarget) {
|
||||||
Utils.createMethodParameters(methodTarget.getMethod())
|
Utils.createMethodParameters(methodTarget.getMethod())
|
||||||
@@ -280,7 +304,7 @@ public class Shell implements CommandRegistry {
|
|||||||
.orElseThrow(() -> new ParameterResolverMissingException(parameter));
|
.orElseThrow(() -> new ParameterResolverMissingException(parameter));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the longest command that can be matched as first word(s) in the given buffer.
|
* 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) {
|
private String findLongestCommand(String prefix) {
|
||||||
String result = methodTargets.keySet().stream()
|
String result = methodTargets.keySet().stream()
|
||||||
.filter(command -> prefix.equals(command) || prefix.startsWith(command + " "))
|
.filter(command -> prefix.equals(command) || prefix.startsWith(command + " "))
|
||||||
.reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2);
|
.reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2);
|
||||||
return "".equals(result) ? null : result;
|
return "".equals(result) ? null : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user