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