Add legacy and default resolvers
This commit is contained in:
38
src/main/java/org/springframework/shell2/Bootstrap.java
Normal file
38
src/main/java/org/springframework/shell2/Bootstrap.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.shell.converters.ArrayConverter;
|
||||
import org.springframework.shell.converters.AvailableCommandsConverter;
|
||||
import org.springframework.shell.converters.SimpleFileConverter;
|
||||
|
||||
/**
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackageClasses = {ArrayConverter.class, Bootstrap.class}, excludeFilters = @ComponentScan.Filter(
|
||||
type = FilterType.ASSIGNABLE_TYPE,
|
||||
value = {AvailableCommandsConverter.class, SimpleFileConverter.class}))
|
||||
public class Bootstrap {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(Bootstrap.class);
|
||||
context.getBean(JLineShell.class).run();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConversionService conversionService() {
|
||||
return new DefaultConversionService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ParameterResolver parameterResolver(ConversionService conversionService) {
|
||||
return new DefaultParameterResolver(conversionService);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 09/12/15.
|
||||
*/
|
||||
@Component
|
||||
public class DefaultMethodTargetResolver implements MethodTargetResolver {
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> resolve(ApplicationContext applicationContext) {
|
||||
Map<String,MethodTarget> methodTargets = new HashMap<>();
|
||||
Map<String, Object> commandBeans = applicationContext.getBeansWithAnnotation(ShellComponent.class);
|
||||
for (Object bean : commandBeans.values()) {
|
||||
Class<?> clazz = bean.getClass();
|
||||
ReflectionUtils.doWithMethods(clazz, method -> {
|
||||
ShellMapping shellMapping = method.getAnnotation(ShellMapping.class);
|
||||
String[] keys = shellMapping.value();
|
||||
if (keys.length == 1 && "".equals(keys[0])) {
|
||||
keys[0] = method.getName();
|
||||
}
|
||||
for (String key : keys) {
|
||||
methodTargets.put(key, new MethodTarget(method, bean, shellMapping.help()));
|
||||
}
|
||||
}, method -> method.getAnnotation(ShellMapping.class) != null);
|
||||
}
|
||||
return methodTargets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 09/12/15.
|
||||
*/
|
||||
class DefaultParameterResolver implements ParameterResolver {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
public DefaultParameterResolver(ConversionService conversionService) {
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter parameter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolve(MethodParameter methodParameter, List<String> words) {
|
||||
TypeDescriptor targetType = new TypeDescriptor(methodParameter);
|
||||
Object converted = conversionService.convert(words.get(methodParameter.getParameterIndex()), TypeDescriptor.valueOf(String.class), targetType);
|
||||
return converted;
|
||||
}
|
||||
}
|
||||
154
src/main/java/org/springframework/shell2/JLineShell.java
Normal file
154
src/main/java/org/springframework/shell2/JLineShell.java
Normal file
@@ -0,0 +1,154 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
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 javax.annotation.PostConstruct;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.executable.ExecutableValidator;
|
||||
|
||||
import org.jline.reader.Candidate;
|
||||
import org.jline.reader.Completer;
|
||||
import org.jline.reader.Highlighter;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.LineReaderBuilder;
|
||||
import org.jline.reader.ParsedLine;
|
||||
import org.jline.reader.impl.DefaultParser;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.TerminalBuilder;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 26/11/15.
|
||||
*/
|
||||
@Component
|
||||
public class JLineShell {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Autowired
|
||||
private ConversionService conversionService;
|
||||
|
||||
private Map<String, Object> commandBeans;
|
||||
|
||||
private Map<String, MethodTarget> methodTargets = new HashMap<>();
|
||||
|
||||
private LineReader lineReader;
|
||||
|
||||
@Autowired
|
||||
private List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws Exception {
|
||||
for (MethodTargetResolver resolver : applicationContext.getBeansOfType(MethodTargetResolver.class).values()) {
|
||||
methodTargets.putAll(resolver.resolve(applicationContext));
|
||||
}
|
||||
|
||||
Terminal terminal = TerminalBuilder.builder().build();
|
||||
LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder()
|
||||
.terminal(terminal)
|
||||
.appName("Foo")
|
||||
.completer(new Completer() {
|
||||
|
||||
@Override
|
||||
public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
|
||||
candidates.add(new Candidate("value", "displayed value", "v", "the description", null, null, false));
|
||||
candidates.add(new Candidate("value1", "displayed value1", "v", "the description of v1", null, null, false));
|
||||
}
|
||||
})
|
||||
.highlighter(new Highlighter() {
|
||||
|
||||
@Override
|
||||
public AttributedString highlight(LineReader reader, String buffer) {
|
||||
if (buffer.length() < 4) {
|
||||
return new AttributedString(buffer, AttributedStyle.DEFAULT.blink().foreground(AttributedStyle.RED));
|
||||
}
|
||||
else {
|
||||
return new AttributedString(buffer);
|
||||
}
|
||||
}
|
||||
})
|
||||
.parser(new DefaultParser());
|
||||
|
||||
lineReader = lineReaderBuilder.build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
while (true) {
|
||||
lineReader.readLine("shell:>");
|
||||
String separator = "";
|
||||
StringBuilder candidateCommand = new StringBuilder();
|
||||
MethodTarget methodTarget = null;
|
||||
int c = 0;
|
||||
int wordsUsedForCommandKey = 0;
|
||||
for (String word : lineReader.getParsedLine().words()) {
|
||||
c++;
|
||||
candidateCommand.append(separator).append(word);
|
||||
MethodTarget t = methodTargets.get(candidateCommand.toString());
|
||||
if (t != null) {
|
||||
methodTarget = t;
|
||||
wordsUsedForCommandKey = c;
|
||||
}
|
||||
separator = " ";
|
||||
}
|
||||
|
||||
List<String> words = lineReader.getParsedLine().words();
|
||||
if (methodTarget != null) {
|
||||
Parameter[] parameters = methodTarget.getMethod().getParameters();
|
||||
Object[] rawArgs = new Object[parameters.length];
|
||||
Object unresolved = new Object();
|
||||
Arrays.fill(rawArgs, unresolved);
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
MethodParameter methodParameter = new MethodParameter(methodTarget.getMethod(), i);
|
||||
for (ParameterResolver resolver : parameterResolvers) {
|
||||
if (resolver.supports(methodParameter)) {
|
||||
rawArgs[i] = resolver.resolve(methodParameter, words.subList(wordsUsedForCommandKey, words.size()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rawArgs[i] == unresolved) {
|
||||
throw new IllegalStateException("Could not resolve " + methodParameter);
|
||||
}
|
||||
}
|
||||
|
||||
ExecutableValidator executableValidator = Validation
|
||||
.buildDefaultValidatorFactory().getValidator().forExecutables();
|
||||
Set<ConstraintViolation<Object>> constraintViolations = executableValidator.validateParameters(methodTarget.getBean(),
|
||||
methodTarget.getMethod(),
|
||||
rawArgs);
|
||||
System.out.println(constraintViolations);
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = ReflectionUtils.invokeMethod(methodTarget.getMethod(), methodTarget.getBean(), rawArgs);
|
||||
}
|
||||
catch (Exception e) {
|
||||
result = e;
|
||||
}
|
||||
|
||||
System.out.println(String.valueOf(result));
|
||||
|
||||
}
|
||||
else {
|
||||
System.out.println("No command found for " + words);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/main/java/org/springframework/shell2/MethodTarget.java
Normal file
54
src/main/java/org/springframework/shell2/MethodTarget.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 27/11/15.
|
||||
*/
|
||||
public class MethodTarget {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Object bean;
|
||||
|
||||
private final String help;
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help) {
|
||||
this.method = method;
|
||||
this.bean = bean;
|
||||
this.help = help;
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public Object getBean() {
|
||||
return bean;
|
||||
}
|
||||
|
||||
public String getHelp() {
|
||||
return help;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
MethodTarget that = (MethodTarget) o;
|
||||
|
||||
if (!method.equals(that.method)) return false;
|
||||
if (!bean.equals(that.bean)) return false;
|
||||
return help.equals(that.help);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = method.hashCode();
|
||||
result = 31 * result + bean.hashCode();
|
||||
result = 31 * result + help.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 09/12/15.
|
||||
*/
|
||||
public interface MethodTargetResolver {
|
||||
|
||||
public Map<String, MethodTarget> resolve(ApplicationContext context);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 27/11/15.
|
||||
*/
|
||||
public interface ParameterResolver {
|
||||
|
||||
boolean supports(MethodParameter parameter);
|
||||
|
||||
public Object resolve(MethodParameter methodParameter, List<String> words);
|
||||
|
||||
}
|
||||
21
src/main/java/org/springframework/shell2/ShellComponent.java
Normal file
21
src/main/java/org/springframework/shell2/ShellComponent.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 27/11/15.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Documented
|
||||
@Component
|
||||
public @interface ShellComponent {
|
||||
|
||||
String value();
|
||||
}
|
||||
19
src/main/java/org/springframework/shell2/ShellMapping.java
Normal file
19
src/main/java/org/springframework/shell2/ShellMapping.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 27/11/15.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD})
|
||||
public @interface ShellMapping {
|
||||
|
||||
String[] value() default "";
|
||||
|
||||
String help() default "";
|
||||
|
||||
}
|
||||
18
src/main/java/org/springframework/shell2/ShellOption.java
Normal file
18
src/main/java/org/springframework/shell2/ShellOption.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.springframework.shell2;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 27/11/15.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
public @interface ShellOption {
|
||||
|
||||
String[] value() default "";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.springframework.shell2.legacy;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.shell.core.CommandMarker;
|
||||
import org.springframework.shell.core.annotation.CliCommand;
|
||||
import org.springframework.shell2.MethodTarget;
|
||||
import org.springframework.shell2.MethodTargetResolver;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 09/12/15.
|
||||
*/
|
||||
@Component
|
||||
public class LegacyMethodTargetResolver implements MethodTargetResolver {
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> resolve(ApplicationContext context) {
|
||||
Map<String, MethodTarget> methodTargets = new HashMap<>();
|
||||
Map<String, CommandMarker> beansOfType = context.getBeansOfType(CommandMarker.class);
|
||||
for (Object bean : beansOfType.values()) {
|
||||
Class<?> clazz = bean.getClass();
|
||||
ReflectionUtils.doWithMethods(clazz, method -> {
|
||||
CliCommand shellMapping = method.getAnnotation(CliCommand.class);
|
||||
for (String key : shellMapping.value()) {
|
||||
methodTargets.put(key, new MethodTarget(method, bean, shellMapping.help()));
|
||||
}
|
||||
}, method -> method.getAnnotation(CliCommand.class) != null);
|
||||
}
|
||||
return methodTargets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.springframework.shell2.legacy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell.core.Converter;
|
||||
import org.springframework.shell.core.annotation.CliOption;
|
||||
import org.springframework.shell2.ParameterResolver;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 09/12/15.
|
||||
*/
|
||||
@Component
|
||||
public class LegacyParameterResolver implements ParameterResolver {
|
||||
|
||||
public static final Object UNSPECIFIED = new Object();
|
||||
|
||||
@Autowired(required = false)
|
||||
private Collection<Converter<?>> converters = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(CliOption.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolve(MethodParameter methodParameter, List<String> words) {
|
||||
CliOption cliOption = methodParameter.getParameterAnnotation(CliOption.class);
|
||||
Optional<Converter<?>> converter = converters.stream()
|
||||
.filter(c -> c.supports(methodParameter.getParameterType(), cliOption.optionContext()))
|
||||
.findFirst();
|
||||
|
||||
Map<String, String> values = parseOptions(words);
|
||||
Map<String, Object> seenValues = convertValues(values, methodParameter, converter);
|
||||
switch (seenValues.size()) {
|
||||
case 0:
|
||||
if (!cliOption.mandatory()) {
|
||||
String value = cliOption.unspecifiedDefaultValue();
|
||||
return converter
|
||||
.orElseThrow(noConverterFound(cliOption.key()[0], value, methodParameter.getParameterType()))
|
||||
.convertFromText(value, methodParameter.getParameterType(), cliOption.optionContext());
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Could not find parameter values for " + prettifyKeys(Arrays.asList(cliOption.key())) + " in " + words);
|
||||
}
|
||||
case 1:
|
||||
return seenValues.values().iterator().next();
|
||||
default:
|
||||
throw new RuntimeException("Option has been set multiple times via " + prettifyKeys(seenValues.keySet()));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> parseOptions(List<String> words) {
|
||||
Map<String, String> values = new HashMap<>();
|
||||
for (int i = 0; i < words.size(); i++) {
|
||||
String word = words.get(i);
|
||||
if (word.startsWith("--")) {
|
||||
String key = word.substring("--".length());
|
||||
// If next word doesn't exist or starts with '--', this is an unary option. Store null
|
||||
String value = i < words.size() - 1 && !words.get(i+1).startsWith("--") ? words.get(++i) : null;
|
||||
Assert.isTrue(!values.containsKey(key), String.format("Option --%s has already been set", key));
|
||||
values.put(key, value);
|
||||
} // Must be the 'anonymous' option
|
||||
else {
|
||||
Assert.isTrue(!values.containsKey(""), "Anonymous option has already been set");
|
||||
values.put("", word);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private Map<String, Object> convertValues(Map<String, String> values, MethodParameter methodParameter, Optional<Converter<?>> converter) {
|
||||
Map<String, Object> seenValues = new HashMap<>();
|
||||
CliOption option = methodParameter.getParameterAnnotation(CliOption.class);
|
||||
for (String key : option.key()) {
|
||||
if (values.containsKey(key)) {
|
||||
String value = values.get(key);
|
||||
if (value == null && !"__NULL__".equals(option.specifiedDefaultValue())) {
|
||||
value = option.specifiedDefaultValue();
|
||||
}
|
||||
Class<?> parameterType = methodParameter.getParameterType();
|
||||
seenValues.put(key, converter
|
||||
.orElseThrow(noConverterFound(key, value, parameterType))
|
||||
.convertFromText(value, parameterType, option.optionContext()));
|
||||
}
|
||||
}
|
||||
return seenValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of possible keys for an option, suitable for displaying in an error message.
|
||||
*/
|
||||
private String prettifyKeys(Collection<String> keys) {
|
||||
return keys.stream().map(s -> "".equals(s) ? "<anonymous>" : "--" + s).collect(Collectors.joining(", ", "[", "]"));
|
||||
}
|
||||
|
||||
private Supplier<IllegalStateException> noConverterFound(String key, String value, Class<?> parameterType) {
|
||||
return () -> new IllegalStateException("No converter found for --" + key + " from '" + value + "' to type " + parameterType);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user