diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..149242e9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+/.classpath
+/.project
+.settings/
+.gradle
+build
+target/
+/log.roo
+
+/bin/
+/*.log
+
+/.idea
+/*.iml
+/*.ipr
+/*.iws
+out
+.gradletasknamecache
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 00000000..9d56f8c7
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,52 @@
+
+ 4.0.0
+
+ org.springframework.shell
+ spring-shell2
+ 2.0.0.BUILD-SNAPSHOT
+ jar
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.3.0.RELEASE
+
+
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ jline
+ jline
+ 3.0.0-SNAPSHOT
+
+
+ org.springframework.shell
+ spring-shell
+ 1.2.0.BUILD-SNAPSHOT
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/src/main/java/org/springframework/shell2/Bootstrap.java b/src/main/java/org/springframework/shell2/Bootstrap.java
new file mode 100644
index 00000000..8a1ea7d3
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/Bootstrap.java
@@ -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);
+ }
+
+}
diff --git a/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java b/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java
new file mode 100644
index 00000000..562de0f3
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java
@@ -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 resolve(ApplicationContext applicationContext) {
+ Map methodTargets = new HashMap<>();
+ Map 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;
+ }
+}
diff --git a/src/main/java/org/springframework/shell2/DefaultParameterResolver.java b/src/main/java/org/springframework/shell2/DefaultParameterResolver.java
new file mode 100644
index 00000000..829a150f
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/DefaultParameterResolver.java
@@ -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 words) {
+ TypeDescriptor targetType = new TypeDescriptor(methodParameter);
+ Object converted = conversionService.convert(words.get(methodParameter.getParameterIndex()), TypeDescriptor.valueOf(String.class), targetType);
+ return converted;
+ }
+}
diff --git a/src/main/java/org/springframework/shell2/JLineShell.java b/src/main/java/org/springframework/shell2/JLineShell.java
new file mode 100644
index 00000000..2b404275
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/JLineShell.java
@@ -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 commandBeans;
+
+ private Map methodTargets = new HashMap<>();
+
+ private LineReader lineReader;
+
+ @Autowired
+ private List 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 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 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> 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);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/shell2/MethodTarget.java b/src/main/java/org/springframework/shell2/MethodTarget.java
new file mode 100644
index 00000000..1a18271a
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/MethodTarget.java
@@ -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;
+ }
+}
diff --git a/src/main/java/org/springframework/shell2/MethodTargetResolver.java b/src/main/java/org/springframework/shell2/MethodTargetResolver.java
new file mode 100644
index 00000000..9997ab3d
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/MethodTargetResolver.java
@@ -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 resolve(ApplicationContext context);
+
+}
diff --git a/src/main/java/org/springframework/shell2/ParameterResolver.java b/src/main/java/org/springframework/shell2/ParameterResolver.java
new file mode 100644
index 00000000..2f15e5dc
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/ParameterResolver.java
@@ -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 words);
+
+}
diff --git a/src/main/java/org/springframework/shell2/ShellComponent.java b/src/main/java/org/springframework/shell2/ShellComponent.java
new file mode 100644
index 00000000..1dfce3d5
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/ShellComponent.java
@@ -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();
+}
diff --git a/src/main/java/org/springframework/shell2/ShellMapping.java b/src/main/java/org/springframework/shell2/ShellMapping.java
new file mode 100644
index 00000000..bae44382
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/ShellMapping.java
@@ -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 "";
+
+}
diff --git a/src/main/java/org/springframework/shell2/ShellOption.java b/src/main/java/org/springframework/shell2/ShellOption.java
new file mode 100644
index 00000000..1abb0ffd
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/ShellOption.java
@@ -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 "";
+}
diff --git a/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java b/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java
new file mode 100644
index 00000000..67917c84
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java
@@ -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 resolve(ApplicationContext context) {
+ Map methodTargets = new HashMap<>();
+ Map 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;
+ }
+}
diff --git a/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java b/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java
new file mode 100644
index 00000000..3914cbad
--- /dev/null
+++ b/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java
@@ -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> converters = new ArrayList<>();
+
+ @Override
+ public boolean supports(MethodParameter parameter) {
+ return parameter.hasParameterAnnotation(CliOption.class);
+ }
+
+ @Override
+ public Object resolve(MethodParameter methodParameter, List words) {
+ CliOption cliOption = methodParameter.getParameterAnnotation(CliOption.class);
+ Optional> converter = converters.stream()
+ .filter(c -> c.supports(methodParameter.getParameterType(), cliOption.optionContext()))
+ .findFirst();
+
+ Map values = parseOptions(words);
+ Map 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 parseOptions(List words) {
+ Map 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 convertValues(Map values, MethodParameter methodParameter, Optional> converter) {
+ Map 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 keys) {
+ return keys.stream().map(s -> "".equals(s) ? "" : "--" + s).collect(Collectors.joining(", ", "[", "]"));
+ }
+
+ private Supplier noConverterFound(String key, String value, Class> parameterType) {
+ return () -> new IllegalStateException("No converter found for --" + key + " from '" + value + "' to type " + parameterType);
+ }
+
+}
diff --git a/src/test/java/org/springframework/shell2/legacy/ArtifactType.java b/src/test/java/org/springframework/shell2/legacy/ArtifactType.java
new file mode 100644
index 00000000..0ea4058a
--- /dev/null
+++ b/src/test/java/org/springframework/shell2/legacy/ArtifactType.java
@@ -0,0 +1,9 @@
+package org.springframework.shell2.legacy;
+
+/**
+ * Created by ericbottard on 09/12/15.
+ */
+public enum ArtifactType {
+
+ source, processor, sink, task
+}
diff --git a/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java b/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java
new file mode 100644
index 00000000..7debca35
--- /dev/null
+++ b/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java
@@ -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;
+ }
+
+}
diff --git a/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java b/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java
new file mode 100644
index 00000000..6eb2270d
--- /dev/null
+++ b/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java
@@ -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 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();
+ }
+ }
+
+}
diff --git a/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java b/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java
new file mode 100644
index 00000000..9e954faf
--- /dev/null
+++ b/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java
@@ -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 stringConverter() {
+ return new StringConverter();
+ }
+
+ @Bean
+ public Converter booleanConverter() {
+ return new BooleanConverter();
+ }
+
+ @Bean
+ public Converter> enumConverter() {
+ return new EnumConverter();
+ }
+
+ @Bean
+ public ParameterResolver parameterResolver() {
+ return new LegacyParameterResolver();
+ }
+ }
+}