Add legacy and default resolvers
This commit is contained in:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/.classpath
|
||||
/.project
|
||||
.settings/
|
||||
.gradle
|
||||
build
|
||||
target/
|
||||
/log.roo
|
||||
|
||||
/bin/
|
||||
/*.log
|
||||
|
||||
/.idea
|
||||
/*.iml
|
||||
/*.ipr
|
||||
/*.iws
|
||||
out
|
||||
.gradletasknamecache
|
||||
52
pom.xml
Normal file
52
pom.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.shell</groupId>
|
||||
<artifactId>spring-shell2</artifactId>
|
||||
<version>2.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>1.3.0.RELEASE</version>
|
||||
</parent>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jline</groupId>
|
||||
<artifactId>jline</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.shell</groupId>
|
||||
<artifactId>spring-shell</artifactId>
|
||||
<version>1.2.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.springframework.shell2.legacy;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 09/12/15.
|
||||
*/
|
||||
public enum ArtifactType {
|
||||
|
||||
source, processor, sink, task
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.springframework.shell2.legacy;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.shell.core.CommandMarker;
|
||||
import org.springframework.shell.core.annotation.CliCommand;
|
||||
import org.springframework.shell.core.annotation.CliOption;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 09/12/15.
|
||||
*/
|
||||
public class LegacyCommands implements CommandMarker {
|
||||
|
||||
public static final Method REGISTER_METHOD = ReflectionUtils.findMethod(LegacyCommands.class, "register", String.class, ArtifactType.class, String.class, boolean.class);
|
||||
public static final Method SUM_METHOD = ReflectionUtils.findMethod(LegacyCommands.class, "sum", int.class, int.class);
|
||||
|
||||
@CliCommand(value = "register module", help = "Register a new module")
|
||||
public String register(
|
||||
@CliOption(mandatory = true,
|
||||
key = {"", "name"},
|
||||
help = "the name for the registered module")
|
||||
String name,
|
||||
@CliOption(mandatory = true,
|
||||
key = {"type"},
|
||||
help = "the type for the registered module")
|
||||
ArtifactType type,
|
||||
@CliOption(mandatory = true,
|
||||
key = {"coordinates", "coords"},
|
||||
help = "coordinates to the module archive")
|
||||
String coordinates,
|
||||
@CliOption(key = "force",
|
||||
help = "force update if module already exists (only if not in use)",
|
||||
specifiedDefaultValue = "true",
|
||||
unspecifiedDefaultValue = "false")
|
||||
boolean force) {
|
||||
return String.format(("Successfully registered module '%s:%s'"), type, name);
|
||||
}
|
||||
|
||||
@CliCommand("sum")
|
||||
public int sum(@CliOption(key = "v1", unspecifiedDefaultValue = "38") int a, @CliOption(key = "v2", specifiedDefaultValue = "42") int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.springframework.shell2.legacy;
|
||||
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.hamcrest.collection.IsMapContaining;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell2.MethodTarget;
|
||||
import org.springframework.shell2.MethodTargetResolver;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* Created by ericbottard on 09/12/15.
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = LegacyMethodTargetResolverTest.Config.class)
|
||||
public class LegacyMethodTargetResolverTest {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Autowired
|
||||
private LegacyCommands legacyCommands;
|
||||
|
||||
@Autowired
|
||||
private MethodTargetResolver resolver;
|
||||
|
||||
@Test
|
||||
public void findsMethodsAnnotatedWithCliCommand() throws Exception {
|
||||
Map<String, MethodTarget> targets = resolver.resolve(applicationContext);
|
||||
|
||||
assertThat(targets, IsMapContaining.hasEntry(
|
||||
"register module",
|
||||
new MethodTarget(LegacyCommands.REGISTER_METHOD, legacyCommands, "Register a new module" )
|
||||
));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public LegacyCommands legacyCommands() {
|
||||
return new LegacyCommands();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MethodTargetResolver methodTargetResolver() {
|
||||
return new LegacyMethodTargetResolver();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package org.springframework.shell2.legacy;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell.converters.BooleanConverter;
|
||||
import org.springframework.shell.converters.EnumConverter;
|
||||
import org.springframework.shell.converters.StringConverter;
|
||||
import org.springframework.shell.core.Converter;
|
||||
import org.springframework.shell2.ParameterResolver;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = LegacyParameterResolverTest.Config.class)
|
||||
public class LegacyParameterResolverTest {
|
||||
|
||||
private static final int NAME_OR_ANONYMOUS = 0;
|
||||
private static final int TYPE = 1;
|
||||
private static final int COORDINATES = 2;
|
||||
private static final int FORCE = 3;
|
||||
|
||||
@Autowired
|
||||
ParameterResolver parameterResolver;
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void supportsParameterAnnotatedWithCliOption() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.REGISTER_METHOD, NAME_OR_ANONYMOUS);
|
||||
|
||||
boolean supports = parameterResolver.supports(methodParameter);
|
||||
|
||||
assertThat(supports, is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesParameterAnnotatedWithCliOption() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.REGISTER_METHOD, NAME_OR_ANONYMOUS);
|
||||
|
||||
Object result = parameterResolver.resolve(methodParameter, asList("--foo bar --name baz --qix bux".split(" ")));
|
||||
|
||||
assertThat(result, is("baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesAnonymousParameterAnnotatedWithCliOption() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.REGISTER_METHOD, NAME_OR_ANONYMOUS);
|
||||
|
||||
Object result = parameterResolver.resolve(methodParameter, asList("--foo bar baz --qix bux".split(" ")));
|
||||
assertThat(result, is("baz"));
|
||||
|
||||
// As first param
|
||||
result = parameterResolver.resolve(methodParameter, asList("baz --foo bar --qix bux".split(" ")));
|
||||
assertThat(result, is("baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesLegacyConverters() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.REGISTER_METHOD, TYPE);
|
||||
|
||||
Object result = parameterResolver.resolve(methodParameter, asList("--foo bar --name baz --qix bux --type processor".split(" ")));
|
||||
|
||||
assertThat(result, Matchers.is(ArtifactType.processor));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnspecifiedDefaultValue() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.REGISTER_METHOD, FORCE);
|
||||
|
||||
Object result = parameterResolver.resolve(methodParameter, asList("--foo bar --name baz --qix bux".split(" ")));
|
||||
|
||||
assertThat(result, is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpecifiedDefaultValue() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.REGISTER_METHOD, FORCE);
|
||||
|
||||
assertThat(parameterResolver.resolve(methodParameter, asList("--force --foo bar --name baz --qix bux".split(" "))), is(true));
|
||||
assertThat(parameterResolver.resolve(methodParameter, asList("--foo bar --name baz --qix bux --force".split(" "))), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameterNotFound() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.REGISTER_METHOD, COORDINATES);
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.expectMessage("Could not find parameter values for [--coordinates, --coords] in [--force, --foo, bar, --name, baz, --qix, bux]");
|
||||
parameterResolver.resolve(methodParameter, asList("--force --foo bar --name baz --qix bux".split(" ")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameterFoundTooManyTimes() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.REGISTER_METHOD, COORDINATES);
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.expectMessage("Option --coordinates has already been set");
|
||||
// with the exact same name
|
||||
parameterResolver.resolve(methodParameter, asList("--force --coordinates bar --coordinates baz --qix bux".split(" ")));
|
||||
// or even with aliases
|
||||
parameterResolver.resolve(methodParameter, asList("--force --coordinates bar --coords baz --qix bux".split(" ")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoConverterFound() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.SUM_METHOD, 0);
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("No converter found for --v1 from '1' to type int");
|
||||
parameterResolver.resolve(methodParameter, asList("--v1 1 --v2 2".split(" ")));
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoConverterFoundForUnspecifiedValue() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.SUM_METHOD, 0);
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("No converter found for --v1 from '38' to type int");
|
||||
parameterResolver.resolve(methodParameter, asList("--v2 2".split(" ")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoConverterFoundForSpecifiedValue() throws Exception {
|
||||
MethodParameter methodParameter = new MethodParameter(LegacyCommands.SUM_METHOD, 1);
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
thrown.expectMessage("No converter found for --v2 from '42' to type int");
|
||||
parameterResolver.resolve(methodParameter, asList("--v1 1 --v2".split(" ")));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public Converter<String> stringConverter() {
|
||||
return new StringConverter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Converter<Boolean> booleanConverter() {
|
||||
return new BooleanConverter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Converter<Enum<?>> enumConverter() {
|
||||
return new EnumConverter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ParameterResolver parameterResolver() {
|
||||
return new LegacyParameterResolver();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user