@@ -1,234 +0,0 @@
|
||||
/*
|
||||
* 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.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.executable.ExecutableValidator;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Base 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>Also provides hooks for code completion</p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public abstract class AbstractShell implements Shell {
|
||||
|
||||
@Autowired
|
||||
@Qualifier("main")
|
||||
ResultHandler resultHandler;
|
||||
|
||||
@Autowired
|
||||
protected ApplicationContext applicationContext;
|
||||
|
||||
protected Map<String, MethodTarget> methodTargets = new HashMap<>();
|
||||
|
||||
@Autowired
|
||||
protected List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Marker object to distinguish unresolved arguments from {@code null}, which is a valid value.
|
||||
*/
|
||||
protected static final Object UNRESOLVED = new Object();
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> listCommands() {
|
||||
return methodTargets;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void gatherMethodTargets() throws Exception {
|
||||
for (MethodTargetResolver resolver : applicationContext.getBeansOfType(MethodTargetResolver.class).values()) {
|
||||
methodTargets.putAll(resolver.resolve());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void run() throws IOException {
|
||||
while (true) {
|
||||
Input input = readInput();
|
||||
if (input.words().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
String line = input.rawText();
|
||||
List<String> words = input.words();
|
||||
|
||||
String command = findLongestCommand(line);
|
||||
|
||||
if (command != null) {
|
||||
int wordsUsedForCommandKey = command.split(" ").length;
|
||||
MethodTarget methodTarget = methodTargets.get(command);
|
||||
List<String> wordsForArgs = words.subList(wordsUsedForCommandKey, words.size());
|
||||
Method method = methodTarget.getMethod();
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
Object[] args = resolveArgs(method, wordsForArgs);
|
||||
validateArgs(args, methodTarget);
|
||||
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
||||
}
|
||||
catch (Exception e) {
|
||||
result = e;
|
||||
}
|
||||
|
||||
resultHandler.handleResult(result);
|
||||
|
||||
}
|
||||
else {
|
||||
System.out.println("No command found for " + words);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return text entered by user to invoke commands.
|
||||
*/
|
||||
protected abstract Input readInput();
|
||||
|
||||
/**
|
||||
* 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 subclasses.
|
||||
*/
|
||||
public List<CompletionProposal> complete(CompletionContext context) {
|
||||
|
||||
String prefix = context.upToCursor();
|
||||
|
||||
List<CompletionProposal> candidates = new ArrayList<>();
|
||||
// Find the longest match for a command name with words in the buffer
|
||||
String best = findLongestCommand(prefix);
|
||||
if (best == null) { // no command found
|
||||
candidates.addAll(commandsStartingWith(prefix));
|
||||
return candidates;
|
||||
} // if we're here, we're either trying to complete args for command <best> (will fall through)
|
||||
// or trying to complete command whose name starts with <best> (which also happens to be a command)
|
||||
else if (prefix.equals(best)) {
|
||||
candidates.addAll(commandsStartingWith(best));
|
||||
} // valid command (<best>) followed by a suffix (but not necessarily [<space> args*])
|
||||
else if (!prefix.startsWith(best + " ")) {
|
||||
// must be an invalid command, can't do anything
|
||||
return candidates;
|
||||
}
|
||||
|
||||
// Try to complete arguments
|
||||
MethodTarget methodTarget = methodTargets.get(best);
|
||||
Method method = methodTarget.getMethod();
|
||||
return Arrays.stream(method.getParameters())
|
||||
.map(Utils::createMethodParameter)
|
||||
.flatMap(mp -> findResolver(mp).complete(mp, context).stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<CompletionProposal> commandsStartingWith(String prefix) {
|
||||
return methodTargets.entrySet().stream()
|
||||
.filter(e -> e.getKey().startsWith(prefix))
|
||||
.map(e -> toCompletionProposal(e.getKey(), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CompletionProposal toCompletionProposal(String command, MethodTarget methodTarget) {
|
||||
return new CompletionProposal(command)
|
||||
.category("Available commands")
|
||||
.description(methodTarget.getHelp());
|
||||
}
|
||||
|
||||
private void validateArgs(Object[] args, MethodTarget methodTarget) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] == UNRESOLVED) {
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(methodTarget.getMethod(), i);
|
||||
throw new IllegalStateException("Could not resolve " + methodParameter);
|
||||
}
|
||||
}
|
||||
ExecutableValidator executableValidator = Validation
|
||||
.buildDefaultValidatorFactory().getValidator().forExecutables();
|
||||
Set<ConstraintViolation<Object>> constraintViolations = executableValidator.validateParameters(methodTarget.getBean(),
|
||||
methodTarget.getMethod(),
|
||||
args);
|
||||
if (constraintViolations.size() > 0) {
|
||||
System.out.println(constraintViolations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private Object[] resolveArgs(Method method, List<String> wordsForArgs) {
|
||||
Parameter[] parameters = method.getParameters();
|
||||
Object[] args = new Object[parameters.length];
|
||||
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);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
protected ParameterResolver findResolver(MethodParameter parameter) {
|
||||
return parameterResolvers.stream()
|
||||
.filter(resolver -> resolver.supports(parameter))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("resolver not found"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the longest command that can be matched as first word(s) in the given buffer.
|
||||
*
|
||||
* @return a valid command name, or {@literal null} if none matched
|
||||
*/
|
||||
protected String findLongestCommand(String prefix) {
|
||||
String result = methodTargets.keySet().stream()
|
||||
.filter(prefix::startsWith)
|
||||
.reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2);
|
||||
return "".equals(result) ? null : result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -16,11 +16,6 @@
|
||||
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.TerminalBuilder;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@@ -41,7 +36,7 @@ public class Bootstrap {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(Bootstrap.class);
|
||||
context.getBean(JLineShell.class).run();
|
||||
context.getBean(Shell.class).run();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -49,9 +44,4 @@ public class Bootstrap {
|
||||
return new DefaultConversionService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Terminal terminal() throws IOException {
|
||||
return TerminalBuilder.builder().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A result to be handled by the {@link ResultHandler} when no command could be mapped to user input
|
||||
*/
|
||||
public class CommandNotFound extends RuntimeException {
|
||||
|
||||
private final List<String> words;
|
||||
|
||||
public CommandNotFound(List<String> words) {
|
||||
this.words = words;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return String.format("No command found for '%s'", words.stream().collect(Collectors.joining(" ")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2015 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.Map;
|
||||
|
||||
/**
|
||||
* Implementing this interface allows sub-systems (such as the {@literal help} command) to
|
||||
* discover available commands.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public interface CommandRegistry {
|
||||
|
||||
|
||||
/**
|
||||
* Return the mapping from command trigger keywords to implementation.
|
||||
*/
|
||||
public Map<String, MethodTarget> listCommands();
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -26,17 +27,7 @@ import java.util.List;
|
||||
*/
|
||||
public interface Input {
|
||||
|
||||
Input EMPTY = new Input() {
|
||||
@Override
|
||||
public String rawText() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> words() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
};
|
||||
Input EMPTY = () -> "";
|
||||
|
||||
/**
|
||||
* Return the input as entered by the user.
|
||||
@@ -48,5 +39,5 @@ public interface Input {
|
||||
* to parsing rules (for example, handling quoted portions of the readInput as a single
|
||||
* "word")
|
||||
*/
|
||||
List<String> words();
|
||||
default List<String> words() {return "".equals(rawText()) ? Collections.emptyList() : Arrays.asList(rawText().split(" "));}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Represents a shell command behavior, <em>i.e.</em> code to be executed when a command is requested.
|
||||
@@ -37,11 +40,26 @@ public class MethodTarget {
|
||||
Assert.notNull(method, "Method cannot be null");
|
||||
Assert.notNull(bean, "Bean cannot be null");
|
||||
Assert.hasText(help, "Help cannot be blank");
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
this.method = method;
|
||||
this.bean = bean;
|
||||
this.help = help;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, String help) {
|
||||
Set<Method> found = new HashSet<>();
|
||||
ReflectionUtils.doWithMethods(bean.getClass(), found::add, m -> m.getName().equals(name));
|
||||
if (found.size() != 1) {
|
||||
throw new IllegalArgumentException(String.format("Could not find unique method named '%s' on object of class %s. Found %s",
|
||||
name, bean.getClass(), found));
|
||||
}
|
||||
return new MethodTarget(found.iterator().next(), bean, help);
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,20 +16,229 @@
|
||||
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.executable.ExecutableValidator;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Implementing this interface allows sub-systems (such as the {@literal help} command) to
|
||||
* discover available commands.
|
||||
* 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>Also provides hooks for code completion</p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public interface Shell {
|
||||
public class Shell implements CommandRegistry {
|
||||
|
||||
private final InputProvider inputProvider;
|
||||
|
||||
private final ResultHandler resultHandler;
|
||||
|
||||
@Autowired
|
||||
protected ApplicationContext applicationContext;
|
||||
|
||||
protected Map<String, MethodTarget> methodTargets = new HashMap<>();
|
||||
|
||||
@Autowired
|
||||
protected List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Marker object to distinguish unresolved arguments from {@code null}, which is a valid value.
|
||||
*/
|
||||
protected static final Object UNRESOLVED = new Object();
|
||||
|
||||
public Shell(InputProvider inputProvider, ResultHandler resultHandler) {
|
||||
this.inputProvider = inputProvider;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> listCommands() {
|
||||
return methodTargets;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void gatherMethodTargets() throws Exception {
|
||||
for (MethodTargetResolver resolver : applicationContext.getBeansOfType(MethodTargetResolver.class).values()) {
|
||||
methodTargets.putAll(resolver.resolve());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void run() throws IOException {
|
||||
while (true) {
|
||||
Input input;
|
||||
try {
|
||||
input = inputProvider.readInput();
|
||||
}
|
||||
catch (Exception e) {
|
||||
resultHandler.handleResult(e);
|
||||
continue;
|
||||
}
|
||||
if (input.words().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
String line = input.rawText();
|
||||
List<String> words = input.words();
|
||||
|
||||
String command = findLongestCommand(line);
|
||||
|
||||
Object result;
|
||||
if (command != null) {
|
||||
int wordsUsedForCommandKey = command.split(" ").length;
|
||||
MethodTarget methodTarget = methodTargets.get(command);
|
||||
List<String> wordsForArgs = words.subList(wordsUsedForCommandKey, words.size());
|
||||
Method method = methodTarget.getMethod();
|
||||
|
||||
try {
|
||||
Object[] args = resolveArgs(method, wordsForArgs);
|
||||
validateArgs(args, methodTarget);
|
||||
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
||||
}
|
||||
catch (Exception e) {
|
||||
result = e;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result = new CommandNotFound(words);
|
||||
}
|
||||
resultHandler.handleResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
String prefix = context.upToCursor();
|
||||
|
||||
List<CompletionProposal> candidates = new ArrayList<>();
|
||||
// Find the longest match for a command name with words in the buffer
|
||||
String best = findLongestCommand(prefix);
|
||||
if (best == null) { // no command found
|
||||
candidates.addAll(commandsStartingWith(prefix));
|
||||
return candidates;
|
||||
} // if we're here, we're either trying to complete args for command <best> (will fall through)
|
||||
// or trying to complete command whose name starts with <best> (which also happens to be a command)
|
||||
else if (prefix.equals(best)) {
|
||||
candidates.addAll(commandsStartingWith(best));
|
||||
} // valid command (<best>) followed by a suffix (but not necessarily [<space> args*])
|
||||
else if (!prefix.startsWith(best + " ")) {
|
||||
// must be an invalid command, can't do anything
|
||||
return candidates;
|
||||
}
|
||||
|
||||
// Try to complete arguments
|
||||
MethodTarget methodTarget = methodTargets.get(best);
|
||||
Method method = methodTarget.getMethod();
|
||||
return Arrays.stream(method.getParameters())
|
||||
.map(Utils::createMethodParameter)
|
||||
.flatMap(mp -> findResolver(mp).complete(mp, context).stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<CompletionProposal> commandsStartingWith(String prefix) {
|
||||
return methodTargets.entrySet().stream()
|
||||
.filter(e -> e.getKey().startsWith(prefix))
|
||||
.map(e -> toCompletionProposal(e.getKey(), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CompletionProposal toCompletionProposal(String command, MethodTarget methodTarget) {
|
||||
return new CompletionProposal(command)
|
||||
.category("Available commands")
|
||||
.description(methodTarget.getHelp());
|
||||
}
|
||||
|
||||
private void validateArgs(Object[] args, MethodTarget methodTarget) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] == UNRESOLVED) {
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(methodTarget.getMethod(), i);
|
||||
throw new IllegalStateException("Could not resolve " + methodParameter);
|
||||
}
|
||||
}
|
||||
ExecutableValidator executableValidator = Validation
|
||||
.buildDefaultValidatorFactory().getValidator().forExecutables();
|
||||
Set<ConstraintViolation<Object>> constraintViolations = executableValidator.validateParameters(methodTarget.getBean(),
|
||||
methodTarget.getMethod(),
|
||||
args);
|
||||
if (constraintViolations.size() > 0) {
|
||||
System.out.println(constraintViolations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private Object[] resolveArgs(Method method, List<String> wordsForArgs) {
|
||||
Parameter[] parameters = method.getParameters();
|
||||
Object[] args = new Object[parameters.length];
|
||||
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);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private ParameterResolver findResolver(MethodParameter parameter) {
|
||||
return parameterResolvers.stream()
|
||||
.filter(resolver -> resolver.supports(parameter))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("resolver not found"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the mapping from command trigger keywords to implementation.
|
||||
* Returns the longest command that can be matched as first word(s) in the given buffer.
|
||||
*
|
||||
* @return a valid command name, or {@literal null} if none matched
|
||||
*/
|
||||
public Map<String, MethodTarget> listCommands();
|
||||
private String findLongestCommand(String prefix) {
|
||||
String result = methodTargets.keySet().stream()
|
||||
.filter(prefix::startsWith)
|
||||
.reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2);
|
||||
return "".equals(result) ? null : result;
|
||||
}
|
||||
|
||||
public interface InputProvider {
|
||||
/**
|
||||
* Return text entered by user to invoke commands.
|
||||
*/
|
||||
Input readInput();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell2;
|
||||
package org.springframework.shell2.jline;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
@@ -26,6 +26,8 @@ import org.jline.reader.EOFError;
|
||||
import org.jline.reader.ParsedLine;
|
||||
import org.jline.reader.Parser;
|
||||
|
||||
import org.springframework.shell2.CompletingParsedLine;
|
||||
|
||||
/**
|
||||
* Shameful copy-paste of JLine's {@link org.jline.reader.impl.DefaultParser} which
|
||||
* creates {@link CompletingParsedLine}.
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* 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.
|
||||
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell2;
|
||||
package org.springframework.shell2.jline;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -29,12 +30,23 @@ import org.jline.reader.LineReaderBuilder;
|
||||
import org.jline.reader.ParsedLine;
|
||||
import org.jline.reader.UserInterruptException;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.TerminalBuilder;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell2.CompletingParsedLine;
|
||||
import org.springframework.shell2.CompletionContext;
|
||||
import org.springframework.shell2.CompletionProposal;
|
||||
import org.springframework.shell2.ExitRequest;
|
||||
import org.springframework.shell2.Input;
|
||||
import org.springframework.shell2.ResultHandler;
|
||||
import org.springframework.shell2.Shell;
|
||||
|
||||
/**
|
||||
* Shell implementation using JLine to capture input and trigger completions.
|
||||
@@ -42,31 +54,58 @@ import org.springframework.stereotype.Component;
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
*/
|
||||
@Component
|
||||
public class JLineShell extends AbstractShell {
|
||||
|
||||
LineReader lineReader;
|
||||
@Configuration
|
||||
public class JLineShell {
|
||||
|
||||
@Autowired
|
||||
private Terminal terminal;
|
||||
@Qualifier("main")
|
||||
private ResultHandler resultHandler;
|
||||
|
||||
@Bean
|
||||
public Terminal terminal() {
|
||||
try {
|
||||
return TerminalBuilder.builder().build();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new BeanCreationException("Could not create Terminal: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Shell shell() {
|
||||
return new Shell(new JLineInputProvider(lineReader()), resultHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CompleterAdapter completer() {
|
||||
return new CompleterAdapter();
|
||||
}
|
||||
|
||||
/*
|
||||
* Using setter injection to work around a circular dependency.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() throws Exception {
|
||||
public void lateInit() {
|
||||
completer().setShell(shell());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LineReader lineReader() {
|
||||
ExtendedDefaultParser parser = new ExtendedDefaultParser();
|
||||
parser.setEofOnUnclosedQuote(true);
|
||||
parser.setEofOnEscapedNewLine(true);
|
||||
|
||||
LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder()
|
||||
.terminal(terminal)
|
||||
.terminal(terminal())
|
||||
.appName("Foo")
|
||||
.completer(new CompleterAdapter())
|
||||
.completer(completer())
|
||||
.highlighter(new Highlighter() {
|
||||
|
||||
@Override
|
||||
public AttributedString highlight(LineReader reader, String buffer) {
|
||||
int l = 0;
|
||||
String best = null;
|
||||
for (String command : methodTargets.keySet()) {
|
||||
for (String command : shell().listCommands().keySet()) {
|
||||
if (buffer.startsWith(command) && command.length() > l) {
|
||||
l = command.length();
|
||||
best = command;
|
||||
@@ -82,24 +121,7 @@ public class JLineShell extends AbstractShell {
|
||||
})
|
||||
.parser(parser);
|
||||
|
||||
lineReader = lineReaderBuilder.build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Input readInput() {
|
||||
try {
|
||||
lineReader.readLine(new AttributedString("shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)).toAnsi(terminal));
|
||||
}
|
||||
catch (UserInterruptException e) {
|
||||
if (e.getPartialLine().isEmpty()) {
|
||||
resultHandler.handleResult(new ExitRequest(1));
|
||||
} else {
|
||||
return Input.EMPTY;
|
||||
}
|
||||
}
|
||||
return new JLineInput(lineReader.getParsedLine());
|
||||
return lineReaderBuilder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,19 +137,13 @@ public class JLineShell extends AbstractShell {
|
||||
return words;
|
||||
}
|
||||
|
||||
// Overridden so it can be called from CompleterAdapter
|
||||
|
||||
|
||||
@Override
|
||||
public List<CompletionProposal> complete(CompletionContext context) {
|
||||
return super.complete(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* A bridge between JLine's {@link Completer} contract and our own.
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
private class CompleterAdapter implements Completer {
|
||||
public static class CompleterAdapter implements Completer {
|
||||
|
||||
private Shell shell;
|
||||
|
||||
@Override
|
||||
public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
|
||||
@@ -135,7 +151,7 @@ public class JLineShell extends AbstractShell {
|
||||
|
||||
CompletionContext context = new CompletionContext(sanitizeInput(line.words()), line.wordIndex(), line.wordCursor());
|
||||
|
||||
List<CompletionProposal> proposals = JLineShell.this.complete(context);
|
||||
List<CompletionProposal> proposals = shell.complete(context);
|
||||
proposals.stream()
|
||||
.map(p -> new Candidate(
|
||||
cpl.emit(p.value()).toString(),
|
||||
@@ -148,6 +164,36 @@ public class JLineShell extends AbstractShell {
|
||||
)
|
||||
.forEach(candidates::add);
|
||||
}
|
||||
|
||||
public void setShell(Shell shell) {
|
||||
this.shell = shell;
|
||||
}
|
||||
}
|
||||
|
||||
public static class JLineInputProvider implements Shell.InputProvider {
|
||||
|
||||
private final LineReader lineReader;
|
||||
|
||||
public JLineInputProvider(LineReader lineReader) {
|
||||
this.lineReader = lineReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Input readInput() {
|
||||
try {
|
||||
lineReader.readLine(new AttributedString("shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)).toAnsi(lineReader.getTerminal()));
|
||||
}
|
||||
catch (UserInterruptException e) {
|
||||
if (e.getPartialLine().isEmpty()) {
|
||||
throw new ExitRequest(1);
|
||||
} else {
|
||||
return Input.EMPTY;
|
||||
}
|
||||
}
|
||||
return new JLineInput(lineReader.getParsedLine());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static class JLineInput implements Input {
|
||||
@@ -29,14 +29,7 @@ import org.springframework.stereotype.Component;
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class AttributedCharSequenceResultHandler implements ResultHandler<AttributedCharSequence> {
|
||||
|
||||
private final Terminal terminal;
|
||||
|
||||
@Autowired
|
||||
public AttributedCharSequenceResultHandler(Terminal terminal) {
|
||||
this.terminal = terminal;
|
||||
}
|
||||
public class AttributedCharSequenceResultHandler extends TerminalAwareResultHandler implements ResultHandler<AttributedCharSequence> {
|
||||
|
||||
@Override
|
||||
public void handleResult(AttributedCharSequence result) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.result;
|
||||
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.shell2.CommandNotFound;
|
||||
import org.springframework.shell2.ResultHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Used when no command can be matched for user input.
|
||||
*
|
||||
* Simply prints an error message, without printing the exception class.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class CommandNotFoundResultHandler extends TerminalAwareResultHandler implements ResultHandler<CommandNotFound> {
|
||||
|
||||
@Override
|
||||
public void handleResult(CommandNotFound result) {
|
||||
terminal.writer().println(new AttributedString(result.getMessage(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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.result;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Base class for ResultHandlers that rely on JLine's {@link Terminal}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public abstract class TerminalAwareResultHandler {
|
||||
protected Terminal terminal;
|
||||
|
||||
@Autowired
|
||||
public void setTerminal(Terminal terminal) {
|
||||
this.terminal = terminal;
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,11 @@ import org.springframework.stereotype.Component;
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Component
|
||||
public class ThrowableResultHandler implements ResultHandler<Throwable> {
|
||||
public class ThrowableResultHandler extends TerminalAwareResultHandler implements ResultHandler<Throwable> {
|
||||
|
||||
@Override
|
||||
public void handleResult(Throwable result) {
|
||||
System.out.println(new AttributedString(result.toString(),
|
||||
terminal.writer().println(new AttributedString(result.toString(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* 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.Collections;
|
||||
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.UserInterruptException;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.springframework.shell2.result.TypeHierarchyResultHandler;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link JLineShell}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class JLineShellTest {
|
||||
|
||||
private JLineShell shell = new JLineShell();
|
||||
|
||||
@Test
|
||||
public void testCtrlCInterception() throws Exception {
|
||||
shell.lineReader = mock(LineReader.class);
|
||||
AssertingExitRequestResultHandler resultHandler = new AssertingExitRequestResultHandler();
|
||||
shell.resultHandler = resultHandler;
|
||||
when(shell.lineReader.readLine(Mockito.<String>any())).thenThrow(
|
||||
new UserInterruptException("some input"),
|
||||
new UserInterruptException("")
|
||||
);
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
}
|
||||
catch (BreakControlFlow breakControlFlow) {
|
||||
|
||||
}
|
||||
assertThat(resultHandler.exited, is(true));
|
||||
}
|
||||
|
||||
private static class AssertingExitRequestResultHandler implements ResultHandler<ExitRequest> {
|
||||
|
||||
private boolean exited;
|
||||
|
||||
@Override
|
||||
public void handleResult(ExitRequest result) {
|
||||
exited = true;
|
||||
throw new BreakControlFlow();
|
||||
}
|
||||
}
|
||||
|
||||
private static class BreakControlFlow extends RuntimeException {
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Shell}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ShellTest {
|
||||
|
||||
@Rule
|
||||
public MockitoRule mockitoRule = MockitoJUnit.rule();
|
||||
|
||||
@Mock
|
||||
private Shell.InputProvider inputProvider;
|
||||
|
||||
@Mock
|
||||
private ResultHandler resultHandler;
|
||||
|
||||
@Mock
|
||||
private ParameterResolver parameterResolver;
|
||||
|
||||
@InjectMocks
|
||||
private Shell shell;
|
||||
|
||||
private boolean invoked;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
shell.parameterResolvers = Arrays.asList(parameterResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandMatch() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any());
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
}
|
||||
|
||||
Assert.assertTrue(invoked);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandNotFound() throws IOException {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any(CommandNotFound.class));
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("bonjour", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noCommand() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "", () -> "hello world how are you doing ?");
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any());
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
}
|
||||
|
||||
Assert.assertTrue(invoked);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandThrowingAnException() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "fail");
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any(SomeException.class));
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("fail", MethodTarget.of("failing", this, "Will throw an exception"));
|
||||
|
||||
try {
|
||||
shell.run();
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
}
|
||||
|
||||
Assert.assertTrue(invoked);
|
||||
|
||||
}
|
||||
|
||||
private void helloWorld(String a) {
|
||||
invoked = true;
|
||||
}
|
||||
|
||||
private String failing() {
|
||||
invoked = true;
|
||||
throw new SomeException();
|
||||
}
|
||||
|
||||
|
||||
private static class Exit extends RuntimeException {
|
||||
}
|
||||
|
||||
private static class SomeException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -37,7 +37,7 @@ import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell2.MethodTarget;
|
||||
import org.springframework.shell2.ParameterDescription;
|
||||
import org.springframework.shell2.ParameterResolver;
|
||||
import org.springframework.shell2.Shell;
|
||||
import org.springframework.shell2.CommandRegistry;
|
||||
import org.springframework.shell2.standard.CommandValueProvider;
|
||||
import org.springframework.shell2.standard.ShellComponent;
|
||||
import org.springframework.shell2.standard.ShellMethod;
|
||||
@@ -54,7 +54,7 @@ public class Help {
|
||||
|
||||
private final List<ParameterResolver> parameterResolvers;
|
||||
|
||||
private Shell shell;
|
||||
private CommandRegistry commandRegistry;
|
||||
|
||||
@Autowired
|
||||
public Help(List<ParameterResolver> parameterResolvers) {
|
||||
@@ -62,8 +62,8 @@ public class Help {
|
||||
}
|
||||
|
||||
@Autowired // ctor injection impossible b/c of circular dependency
|
||||
public void setShell(Shell shell) {
|
||||
this.shell = shell;
|
||||
public void setCommandRegistry(CommandRegistry commandRegistry) {
|
||||
this.commandRegistry = commandRegistry;
|
||||
}
|
||||
|
||||
@ShellMethod(help = "Display help about available commands.", prefix = "-")
|
||||
@@ -85,7 +85,7 @@ public class Help {
|
||||
* Return a description of a specific command. Uses a layout inspired by *nix man pages.
|
||||
*/
|
||||
private CharSequence documentCommand(String command) {
|
||||
MethodTarget methodTarget = shell.listCommands().get(command);
|
||||
MethodTarget methodTarget = commandRegistry.listCommands().get(command);
|
||||
if (methodTarget == null) {
|
||||
throw new IllegalArgumentException("Unknown command '" + command + "'");
|
||||
}
|
||||
@@ -181,7 +181,7 @@ public class Help {
|
||||
}
|
||||
|
||||
// ALSO KNOWN AS
|
||||
Set<String> aliases = shell.listCommands().entrySet().stream()
|
||||
Set<String> aliases = commandRegistry.listCommands().entrySet().stream()
|
||||
.filter(e -> e.getValue().equals(methodTarget))
|
||||
.map(Map.Entry::getKey)
|
||||
.filter(c -> !command.equals(c))
|
||||
@@ -203,7 +203,7 @@ public class Help {
|
||||
}
|
||||
|
||||
private CharSequence listCommands() {
|
||||
Map<String, Set<String>> groupedByMethodTarget = shell.listCommands().entrySet().stream()
|
||||
Map<String, Set<String>> groupedByMethodTarget = commandRegistry.listCommands().entrySet().stream()
|
||||
.collect(Collectors.groupingBy(e -> e.getValue().getHelp(), // Use help() as the grouping key
|
||||
mapping(Map.Entry::getKey, toCollection(TreeSet::new)))); // accumulate the command 'names' into a sorted set
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.shell2.standard.StandardParameterResolver;
|
||||
import org.springframework.shell2.MethodTarget;
|
||||
import org.springframework.shell2.ParameterResolver;
|
||||
import org.springframework.shell2.Shell;
|
||||
import org.springframework.shell2.CommandRegistry;
|
||||
import org.springframework.shell2.standard.ShellComponent;
|
||||
import org.springframework.shell2.standard.ShellMethod;
|
||||
import org.springframework.shell2.standard.ShellOption;
|
||||
@@ -93,7 +93,7 @@ public class HelpTest {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Shell shell() {
|
||||
public CommandRegistry shell() {
|
||||
return () -> {
|
||||
Map<String, MethodTarget> result = new HashMap<>();
|
||||
Method method = ReflectionUtils.findMethod(Commands.class, "firstCommand", boolean.class, boolean.class, int.class, float[].class);
|
||||
|
||||
@@ -19,15 +19,12 @@ package org.springframework.shell2.standard;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell2.CompletionContext;
|
||||
import org.springframework.shell2.CompletionProposal;
|
||||
import org.springframework.shell2.Shell;
|
||||
import org.springframework.shell2.CommandRegistry;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -38,17 +35,17 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class CommandValueProvider extends ValueProviderSupport {
|
||||
|
||||
private final Shell shell;
|
||||
private final CommandRegistry commandRegistry;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
public CommandValueProvider(Shell shell) {
|
||||
this.shell = shell;
|
||||
public CommandValueProvider(CommandRegistry commandRegistry) {
|
||||
this.commandRegistry = commandRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompletionProposal> complete(MethodParameter parameter, CompletionContext completionContext, String[] hints) {
|
||||
return shell.listCommands().keySet().stream()
|
||||
return commandRegistry.listCommands().keySet().stream()
|
||||
.map(CompletionProposal::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -23,24 +23,19 @@ import static org.mockito.Mockito.when;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell2.CompletionContext;
|
||||
import org.springframework.shell2.CompletionProposal;
|
||||
import org.springframework.shell2.MethodTarget;
|
||||
import org.springframework.shell2.Shell;
|
||||
import org.springframework.shell2.CommandRegistry;
|
||||
import org.springframework.shell2.Utils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
@@ -52,7 +47,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
public class CommandValueProviderTest {
|
||||
|
||||
@Mock
|
||||
private Shell shell;
|
||||
private CommandRegistry shell;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
Reference in New Issue
Block a user