Add legacy and default resolvers

This commit is contained in:
Eric Bottard
2015-12-10 16:30:53 +01:00
parent 3cd5a1be3b
commit 938bd7cfa2
18 changed files with 899 additions and 0 deletions

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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);
}
}
}
}

View 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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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();
}

View 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 "";
}

View 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 "";
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}