diff --git a/pom.xml b/pom.xml index 60e56ec5..8bf4b168 100644 --- a/pom.xml +++ b/pom.xml @@ -30,14 +30,15 @@ spring-boot-starter-test - jline + org.jline jline - 3.0.0-SNAPSHOT + 3.0.1 org.springframework.shell spring-shell 1.2.0.BUILD-SNAPSHOT + true com.beust diff --git a/src/main/java/org/springframework/shell2/Bootstrap.java b/src/main/java/org/springframework/shell2/Bootstrap.java index 0f9301eb..8590b94a 100644 --- a/src/main/java/org/springframework/shell2/Bootstrap.java +++ b/src/main/java/org/springframework/shell2/Bootstrap.java @@ -32,6 +32,8 @@ 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; +import org.springframework.shell2.standard.EnumValueProvider; +import org.springframework.shell2.standard.StandardParameterResolver; /** */ @@ -53,7 +55,7 @@ public class Bootstrap { @Bean public ParameterResolver parameterResolver(ConversionService conversionService) { - return new DefaultParameterResolver(conversionService); + return new StandardParameterResolver(conversionService); } @Bean @@ -61,4 +63,8 @@ public class Bootstrap { return TerminalBuilder.builder().build(); } + @Bean + public EnumValueProvider enumValueProvider() { + return new EnumValueProvider(); + } } diff --git a/src/main/java/org/springframework/shell2/Commands.java b/src/main/java/org/springframework/shell2/Commands.java new file mode 100644 index 00000000..0c9ef3d3 --- /dev/null +++ b/src/main/java/org/springframework/shell2/Commands.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +import org.springframework.shell2.standard.ShellComponent; +import org.springframework.shell2.standard.ShellMethod; + +/** + * Created by ericbottard on 27/12/15. + */ +@ShellComponent("") +public class Commands { + + @ShellMethod(help = "it's cool") + public void foo(String bar) { + + } + + @ShellMethod(help = "it's better") + public void foobar() { + + } + + @ShellMethod(help = "something else") + public void somethingElse() { + + } + + @ShellMethod(help = "add stuff") + public int add(int ahbahdisdonc, int b, int c) { + return ahbahdisdonc + b + c; + } +} diff --git a/src/main/java/org/springframework/shell2/CompletionContext.java b/src/main/java/org/springframework/shell2/CompletionContext.java new file mode 100644 index 00000000..6d550852 --- /dev/null +++ b/src/main/java/org/springframework/shell2/CompletionContext.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents the buffer context in which completion was triggered. + * + * @author Eric Bottard + */ +public class CompletionContext { + + private final List words; + + private final int wordIndex; + + private final int position; + + + public CompletionContext(List words, int wordIndex, int position) { + this.words = words; + this.wordIndex = wordIndex; + this.position = position; + } + + public List getWords() { + return words; + } + + public int getWordIndex() { + return wordIndex; + } + + public int getPosition() { + return position; + } + + public String upToCursor() { + String start = words.subList(0, wordIndex).stream().collect(Collectors.joining(" ")); + if (wordIndex < words.size()) { + start += " " + currentWord().substring(0, position); + } + return start; + } + + /** + * Return the whole word the cursor is in, or {@code null} if the cursor is past the last word. + */ + public String currentWord() { + return wordIndex < words.size() ? words.get(wordIndex) : null; + } + + public String currentWordUpToCursor() { + String currentWord = currentWord(); + return currentWord != null ? currentWord.substring(0, getPosition()) : null; + } +} diff --git a/src/main/java/org/springframework/shell2/CompletionProposal.java b/src/main/java/org/springframework/shell2/CompletionProposal.java new file mode 100644 index 00000000..8cc99351 --- /dev/null +++ b/src/main/java/org/springframework/shell2/CompletionProposal.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +/** + * Represents a proposal for TAB completion, made not only of the text to append, but also metadata about the proposal. + * + * @author Eric Bottard + */ +public class CompletionProposal { + + /** + * The actual value of the proposal. + */ + private String value; + + /** + * The text displayed while the proposal is being considered. + */ + private String displayText; + + /** + * The description for the proposal. + */ + private String description; + + /** + * The category of the proposal, which may be used to group proposals together. + */ + private String category; + + public CompletionProposal(String value) { + this.value = this.displayText = value; + } + + public String value() { + return value; + } + + public void value(String value) { + this.value = value; + } + + public String displayText() { + return displayText; + } + + public void displayText(String displayText) { + this.displayText = displayText; + } + + public String description() { + return description; + } + + public void description(String description) { + this.description = description; + } + + public String category() { + return category; + } + + public void category(String category) { + this.category = category; + } +} diff --git a/src/main/java/org/springframework/shell2/ExitRequest.java b/src/main/java/org/springframework/shell2/ExitRequest.java index f5d4d9b6..ada423a6 100644 --- a/src/main/java/org/springframework/shell2/ExitRequest.java +++ b/src/main/java/org/springframework/shell2/ExitRequest.java @@ -1,39 +1,39 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -/** - * This exception, when thrown and caught, will ask the shell to gracefully shutdown. - * - * @author Eric Bottard - */ -public class ExitRequest extends RuntimeException { - - private final int code; - - public ExitRequest() { - this(0); - } - - public ExitRequest(int code) { - this.code = code; - } - - public int status() { - return 0; - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +/** + * This exception, when thrown and caught, will ask the shell to gracefully shutdown. + * + * @author Eric Bottard + */ +public class ExitRequest extends RuntimeException { + + private final int code; + + public ExitRequest() { + this(0); + } + + public ExitRequest(int code) { + this.code = code; + } + + public int status() { + return 0; + } +} diff --git a/src/main/java/org/springframework/shell2/JLineShell.java b/src/main/java/org/springframework/shell2/JLineShell.java index 98f45edc..724dd025 100644 --- a/src/main/java/org/springframework/shell2/JLineShell.java +++ b/src/main/java/org/springframework/shell2/JLineShell.java @@ -1,199 +1,285 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -import java.io.Closeable; -import java.io.IOException; -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 java.util.stream.Collectors; - -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.utils.AttributedString; -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.MethodParameter; -import org.springframework.shell2.result.ResultHandlers; -import org.springframework.stereotype.Component; -import org.springframework.util.ReflectionUtils; - -/** - * Created by ericbottard on 26/11/15. - */ -@Component -public class JLineShell implements Shell { - - @Autowired - private ResultHandlers resultHandlers; - - @Autowired - private ApplicationContext applicationContext; - - private Map methodTargets = new HashMap<>(); - - private LineReader lineReader; - - @Autowired - private Terminal terminal; - - @Autowired - private List parameterResolvers = new ArrayList<>(); - - @Override - public Map listCommands() { - return methodTargets; - } - - @PostConstruct - public void init() throws Exception { - for (MethodTargetResolver resolver : applicationContext.getBeansOfType(MethodTargetResolver.class).values()) { - methodTargets.putAll(resolver.resolve(applicationContext)); - } - - 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) { - int l = 0; - String best = null; - for (String command : methodTargets.keySet()) { - if (buffer.startsWith(command) && command.length() > l) { - l = command.length(); - best = command; - } - } - if (best != null) { - return new AttributedStringBuilder(buffer.length()).append(best, AttributedStyle.BOLD).append(buffer.substring(l)).toAttributedString(); - } - else { - return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); - } - } - }) - .parser(new DefaultParser()); - - lineReader = lineReaderBuilder.build(); - - } - - - public void run() throws IOException { - 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(); - // TODO investigate trailing empty string in e.g. "help 'WTF 2'" - words = words.stream().filter(w -> w.length() > 0).collect(Collectors.toList()); - 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); - methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - 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); - if (constraintViolations.size() > 0) { - System.out.println(constraintViolations); - } - - Object result = null; - try { - result = ReflectionUtils.invokeMethod(methodTarget.getMethod(), methodTarget.getBean(), rawArgs); - } - catch (ExitRequest e) { - if (applicationContext instanceof Closeable) { - ((Closeable)applicationContext).close(); - System.exit(e.status()); - } - } - catch (Exception e) { - result = e; - } - - resultHandlers.handleResult(result); - - } - else { - System.out.println("No command found for " + words); - } - } - } - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.Method; +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 java.util.stream.Collectors; + +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.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +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.shell2.result.ResultHandlers; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; + +/** + * Created by ericbottard on 26/11/15. + */ +@Component +public class JLineShell implements Shell { + + @Autowired + private ResultHandlers resultHandlers; + + @Autowired + private ApplicationContext applicationContext; + + private Map methodTargets = new HashMap<>(); + + private LineReader lineReader; + + @Autowired + private Terminal terminal; + + @Autowired + private List parameterResolvers = new ArrayList<>(); + + /** + * Marker object to distinguish unresolved arguments from {@code null}, which is a valid value. + */ + private static final Object UNRESOLVED = new Object(); + + @Override + public Map listCommands() { + return methodTargets; + } + + @PostConstruct + public void init() throws Exception { + for (MethodTargetResolver resolver : applicationContext.getBeansOfType(MethodTargetResolver.class).values()) { + methodTargets.putAll(resolver.resolve(applicationContext)); + } + + LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder() + .terminal(terminal) + .appName("Foo") + .completer(new CompleterAdapter()) + .highlighter(new Highlighter() { + + @Override + public AttributedString highlight(LineReader reader, String buffer) { + int l = 0; + String best = null; + for (String command : methodTargets.keySet()) { + if (buffer.startsWith(command) && command.length() > l) { + l = command.length(); + best = command; + } + } + if (best != null) { + return new AttributedStringBuilder(buffer.length()).append(best, AttributedStyle.BOLD).append(buffer.substring(l)).toAttributedString(); + } + else { + return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + } + } + }) + .parser(new DefaultParser()); + + lineReader = lineReaderBuilder.build(); + + } + + + public void run() throws IOException { + while (true) { + lineReader.readLine(new AttributedString("shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)).toAnsi(terminal)); + 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(); + // TODO investigate trailing empty string in e.g. "help 'WTF 2'" + words = words.stream().filter(w -> w.length() > 0).collect(Collectors.toList()); + if (methodTarget != null) { + List wordsForArgs = words.subList(wordsUsedForCommandKey, words.size()); + Method method = methodTarget.getMethod(); + + + Object result = null; + try { + Object[] args = resolveArgs(method, wordsForArgs); + validateArgs(args, methodTarget); + result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args); + } + catch (ExitRequest e) { + if (applicationContext instanceof Closeable) { + ((Closeable) applicationContext).close(); + System.exit(e.status()); + } + } + catch (Exception e) { + result = e; + } + + resultHandlers.handleResult(result); + + } + else { + System.out.println("No command found for " + words); + } + } + } + + private void validateArgs(Object[] args, MethodTarget methodTarget) { + for (int i = 0; i < args.length; i++) { + if (args[i] == UNRESOLVED) { + MethodParameter methodParameter = Utils.createMethodParameter(methodTarget.getMethod(), i); + throw new IllegalStateException("Could not resolve " + methodParameter); + } + } + ExecutableValidator executableValidator = Validation + .buildDefaultValidatorFactory().getValidator().forExecutables(); + Set> constraintViolations = executableValidator.validateParameters(methodTarget.getBean(), + methodTarget.getMethod(), + args); + if (constraintViolations.size() > 0) { + System.out.println(constraintViolations); + } + } + + /** + * Use all known {@link ParameterResolver}s to try to compute a value for each parameter of the method to + * invoke. + * @param method the method for which parameters should be computed + * @param wordsForArgs the list of 'words' that should be converted to parameter values. + * May include markers for passing parameters 'by name' + * @return an array containing resolved parameter values, or {@link #UNRESOLVED} for parameters that could not be + * resolved + */ + private Object[] resolveArgs(Method method, List wordsForArgs) { + Parameter[] parameters = method.getParameters(); + Object[] args = new Object[parameters.length]; + Arrays.fill(args, UNRESOLVED); + for (int i = 0; i < parameters.length; i++) { + MethodParameter methodParameter = Utils.createMethodParameter(method, i); + args[i] = findResolver(methodParameter).resolve(methodParameter, wordsForArgs); + } + return args; + } + + private ParameterResolver findResolver(MethodParameter parameter) { + return parameterResolvers.stream() + .filter(resolver -> resolver.supports(parameter)) + .findFirst() + .orElseThrow(() -> new RuntimeException("resolver not found")); + } + + /** + * A bridge between JLine's {@link Completer} contract and our own. + * @author Eric Bottard + */ + private class CompleterAdapter implements Completer { + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + String prefix = reader.getBuffer().upToCursor(); + + String best = methodTargets.keySet().stream() + .filter(c -> prefix.startsWith(c)) + .reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2); + if (best.equals("")) { // no command found + List result = commandsStartingWith(prefix); + candidates.addAll(result); + return; + } // trying to complete args for command , + // or trying to complete command whose name starts with (which also happens to be a command) + else if (prefix.equals(best)) { + candidates.addAll(commandsStartingWith(best)); + return; + } // valid command () followed by a suffix (but not necessarily [ args*]) + else if (!prefix.startsWith(best + " ")) { + // must be an invalid command, can't do anything + return; + } + + // Try to complete arguments + MethodTarget methodTarget = methodTargets.get(best); + List words = line.words(); + int noOfWordsInCommand = best.split(" ").length; + List rest = words.subList(noOfWordsInCommand, words.size()) + .stream() + .filter(w -> !w.isEmpty()) + .collect(Collectors.toList()); + CompletionContext context = new CompletionContext(rest, line.wordIndex() - noOfWordsInCommand, line.wordCursor()); + Method method = methodTarget.getMethod(); + for (int i = 0; i < method.getParameterCount(); i++) { + MethodParameter methodParameter = Utils.createMethodParameter(method, i); + ParameterResolver resolver = findResolver(methodParameter); + resolver.complete(methodParameter, context) + .stream() + .map(completion -> new Candidate( + completion.value(), + completion.displayText(), + "Comp for parameter " + resolver.describe(methodParameter).toString(), + resolver.describe(methodParameter).help(), + null, null, true) + ) + .forEach(candidates::add); + } + } + + private List commandsStartingWith(String prefix) { + return methodTargets.entrySet().stream() + .filter(e -> e.getKey().startsWith(prefix)) // find commands that start with our buffer prefix + .map(e -> toCandidate(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } + + private Candidate toCandidate(String command, MethodTarget methodTarget) { + return new Candidate(command, command, "Available commands", methodTarget.getHelp(), null, null, true); + } + } +} diff --git a/src/main/java/org/springframework/shell2/MethodTarget.java b/src/main/java/org/springframework/shell2/MethodTarget.java index de124433..846f7ea7 100644 --- a/src/main/java/org/springframework/shell2/MethodTarget.java +++ b/src/main/java/org/springframework/shell2/MethodTarget.java @@ -1,75 +1,75 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -import java.lang.reflect.Method; - -import org.springframework.util.Assert; - -/** - * 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) { - Assert.notNull(method, "Method cannot be null"); - Assert.notNull(bean, "Bean cannot be null"); - Assert.hasText(help, "Help cannot be blank"); - 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; - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +import java.lang.reflect.Method; + +import org.springframework.util.Assert; + +/** + * 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) { + Assert.notNull(method, "Method cannot be null"); + Assert.notNull(bean, "Bean cannot be null"); + Assert.hasText(help, "Help cannot be blank"); + 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 index d2c12adb..b70f693a 100644 --- a/src/main/java/org/springframework/shell2/MethodTargetResolver.java +++ b/src/main/java/org/springframework/shell2/MethodTargetResolver.java @@ -1,30 +1,30 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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); - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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/ParameterDescription.java b/src/main/java/org/springframework/shell2/ParameterDescription.java index e4870b74..e79113d4 100644 --- a/src/main/java/org/springframework/shell2/ParameterDescription.java +++ b/src/main/java/org/springframework/shell2/ParameterDescription.java @@ -1,125 +1,137 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -import java.util.List; -import java.util.Optional; - -import org.springframework.core.MethodParameter; - -/** - * Encapsulates information about a shell invokable method parameter, so that it can be documented. - * - *

Instances of this class are constructed by {@link ParameterResolver#describe(MethodParameter)}.

- * - * @author Eric Bottard - */ -public class ParameterDescription { - - /** - * A string representation of the type of the parameter. - */ - private final String type; - - /** - * A string representation of the parameter, as it should appear in a parameter list. - * If not provided, this is derived from the parameter type. - */ - private String formal; - - /** - * A string representation of the default value for the parameter, if any. - */ - private Optional defaultValue = Optional.empty(); - - /** - * The list of 'keys' that can be used to specify this parameter, if any. - */ - private List keys; - - /** - * Depending on the {@link ParameterResolver}, whether keys are mandatory to identify this parameter. - */ - private boolean mandatoryKey = true; - - /** - * A short description of this parameter. - */ - private String help = ""; - - public ParameterDescription(String type) { - this.type = type; - this.formal = type; - } - - public static ParameterDescription ofType(String type) { - return new ParameterDescription(Utils.unCamelify(type)); - } - - public static ParameterDescription ofType(Class type) { - return ofType(type.getSimpleName()); - } - - public ParameterDescription help(String help) { - this.help = help; - return this; - } - - public boolean mandatoryKey() { - return mandatoryKey; - } - - public List keys() { - return keys; - } - - public Optional defaultValue() { - return defaultValue; - } - - public ParameterDescription defaultValue(String defaultValue) { - this.defaultValue = Optional.of(defaultValue); - return this; - } - - public ParameterDescription keys(List keys) { - this.keys = keys; - return this; - } - - public ParameterDescription mandatoryKey(boolean mandatoryKey) { - this.mandatoryKey = mandatoryKey; - return this; - } - - public String type() { - return type; - } - - public String formal() { - return formal; - } - - public String help() { - return help; - } - - public ParameterDescription formal(String formal) { - this.formal = formal; - return this; - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +import java.util.List; +import java.util.Optional; + +import org.springframework.core.MethodParameter; + +/** + * Encapsulates information about a shell invokable method parameter, so that it can be documented. + * + *

Instances of this class are constructed by {@link ParameterResolver#describe(MethodParameter)}.

+ * + * @author Eric Bottard + */ +public class ParameterDescription { + + /** + * The original method parameter this is describing. + */ + private final MethodParameter parameter; + + /** + * A string representation of the type of the parameter. + */ + private final String type; + + /** + * A string representation of the parameter, as it should appear in a parameter list. + * If not provided, this is derived from the parameter type. + */ + private String formal; + + /** + * A string representation of the default value for the parameter, if any. + */ + private Optional defaultValue = Optional.empty(); + + /** + * The list of 'keys' that can be used to specify this parameter, if any. + */ + private List keys; + + /** + * Depending on the {@link ParameterResolver}, whether keys are mandatory to identify this parameter. + */ + private boolean mandatoryKey = true; + + /** + * A short description of this parameter. + */ + private String help = ""; + + public ParameterDescription(MethodParameter parameter, String type) { + this.parameter = parameter; + this.type = type; + this.formal = type; + } + + public static ParameterDescription outOf(MethodParameter parameter) { + Class type = parameter.getParameterType(); + return new ParameterDescription(parameter, Utils.unCamelify(type.getSimpleName())); + } + + public ParameterDescription help(String help) { + this.help = help; + return this; + } + + public boolean mandatoryKey() { + return mandatoryKey; + } + + public List keys() { + return keys; + } + + public Optional defaultValue() { + return defaultValue; + } + + public ParameterDescription defaultValue(String defaultValue) { + this.defaultValue = Optional.of(defaultValue); + return this; + } + + public ParameterDescription keys(List keys) { + this.keys = keys; + return this; + } + + public ParameterDescription mandatoryKey(boolean mandatoryKey) { + this.mandatoryKey = mandatoryKey; + return this; + } + + public String type() { + return type; + } + + public String formal() { + return formal; + } + + public String help() { + return help; + } + + public ParameterDescription formal(String formal) { + this.formal = formal; + return this; + } + + @Override + public String toString() { + return String.format("%s %s", keys().iterator().next(), formal()); + } + + public MethodParameter parameter() { + return parameter; + } +} diff --git a/src/test/java/org/springframework/shell2/Remote.java b/src/main/java/org/springframework/shell2/ParameterMissingResolutionException.java similarity index 53% rename from src/test/java/org/springframework/shell2/Remote.java rename to src/main/java/org/springframework/shell2/ParameterMissingResolutionException.java index 3241de4e..ca1780ed 100644 --- a/src/test/java/org/springframework/shell2/Remote.java +++ b/src/main/java/org/springframework/shell2/ParameterMissingResolutionException.java @@ -1,41 +1,40 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -/** - * An example commands class. - * - * @author Eric Bottard - * @author Florent Biville - */ -public class Remote { - - /** - * A command method that showcases
    - *
  • default handling for booleans (force)
  • - *
  • default parameter name discovery (name)
  • - *
  • default value supplying (foo and bar)
  • - *
- */ - @ShellMethod - public void zap(boolean force, - String name, - @ShellOption(defaultValue="defoolt") String foo, - @ShellOption(value = {"bar", "baz"}, defaultValue = "last") String bar) { - - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +/** + * Thrown by a {@link ParameterResolver} when a parameter that should have been set has been left out altogether. + * + * @author Eric Bottard + */ +public class ParameterMissingResolutionException extends RuntimeException { + + private final ParameterDescription parameterDescription; + + public ParameterMissingResolutionException(ParameterDescription parameterDescription) { + this.parameterDescription = parameterDescription; + } + + public ParameterDescription getParameterDescription() { + return parameterDescription; + } + + @Override + public String getMessage() { + return String.format("Parameter '%s' should be specified", parameterDescription); + } +} diff --git a/src/main/java/org/springframework/shell2/ParameterResolver.java b/src/main/java/org/springframework/shell2/ParameterResolver.java index 5697abb0..7fddaa62 100644 --- a/src/main/java/org/springframework/shell2/ParameterResolver.java +++ b/src/main/java/org/springframework/shell2/ParameterResolver.java @@ -1,34 +1,36 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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); - - Object resolve(MethodParameter methodParameter, List words); - - ParameterDescription describe(MethodParameter parameter); - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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); + + Object resolve(MethodParameter methodParameter, List words); + + ParameterDescription describe(MethodParameter parameter); + + List complete(MethodParameter parameter, CompletionContext context); + +} diff --git a/src/main/java/org/springframework/shell2/Shell.java b/src/main/java/org/springframework/shell2/Shell.java index 5a82dcd7..61ceb759 100644 --- a/src/main/java/org/springframework/shell2/Shell.java +++ b/src/main/java/org/springframework/shell2/Shell.java @@ -1,29 +1,29 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -import java.util.Map; - -/** - * Created by ericbottard on 17/12/15. - */ -public interface Shell { - - - public Map listCommands(); - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +import java.util.Map; + +/** + * Created by ericbottard on 17/12/15. + */ +public interface Shell { + + + public Map listCommands(); + +} diff --git a/src/main/java/org/springframework/shell2/UnfinishedParameterResolutionException.java b/src/main/java/org/springframework/shell2/UnfinishedParameterResolutionException.java new file mode 100644 index 00000000..f5057a38 --- /dev/null +++ b/src/main/java/org/springframework/shell2/UnfinishedParameterResolutionException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +/** + * Thrown during parameter resolution, when a parameter has been identified, but could not be correctly resolved. + * + * @author Eric Bottard + */ +public class UnfinishedParameterResolutionException extends RuntimeException { + + private final ParameterDescription parameterDescription; + + private final CharSequence input; + + public UnfinishedParameterResolutionException(ParameterDescription parameterDescription, CharSequence input) { + this.parameterDescription = parameterDescription; + this.input = input; + } + + public ParameterDescription getParameterDescription() { + return parameterDescription; + } + + public CharSequence getInput() { + return input; + } + + @Override + public String getMessage() { + return String.format("Error trying to resolve '%s' using [%s]", parameterDescription, input); + } +} diff --git a/src/main/java/org/springframework/shell2/Utils.java b/src/main/java/org/springframework/shell2/Utils.java index 9f05a7ed..3f91319c 100644 --- a/src/main/java/org/springframework/shell2/Utils.java +++ b/src/main/java/org/springframework/shell2/Utils.java @@ -1,43 +1,79 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -/** - * Some text utilities. - * - * @author Eric Bottard - */ -public class Utils { - - /** - * Turn CamelCaseText into gnu-style-lowercase. - */ - public static String unCamelify(CharSequence original) { - StringBuilder result = new StringBuilder(original.length()); - boolean wasLowercase = false; - for (int i = 0; i < original.length(); i++) { - char ch = original.charAt(i); - if (Character.isUpperCase(ch) && wasLowercase) { - result.append('-'); - } - wasLowercase = Character.isLowerCase(ch); - result.append(Character.toLowerCase(ch)); - } - return result.toString(); - } - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; + +/** + * Some text utilities. + * + * @author Eric Bottard + */ +public class Utils { + + /** + * Turn CamelCaseText into gnu-style-lowercase. + */ + public static String unCamelify(CharSequence original) { + StringBuilder result = new StringBuilder(original.length()); + boolean wasLowercase = false; + for (int i = 0; i < original.length(); i++) { + char ch = original.charAt(i); + if (Character.isUpperCase(ch) && wasLowercase) { + result.append('-'); + } + wasLowercase = Character.isLowerCase(ch); + result.append(Character.toLowerCase(ch)); + } + return result.toString(); + } + + /** + * Convert from JDK {@link Parameter} to Spring {@link MethodParameter}. + */ + public static MethodParameter createMethodParameter(Parameter parameter) { + Parameter[] parameters = parameter.getDeclaringExecutable().getParameters(); + for (int i = 0; i < parameters.length; i++) { + if (parameters[i].equals(parameter)) { + return createMethodParameter(parameter.getDeclaringExecutable(), i); + } + } + throw new AssertionError("Can't happen"); + } + /** + * Return a properly initialized MethodParameter for the given executable and index. + */ + public static MethodParameter createMethodParameter(Executable executable, int i) { + MethodParameter methodParameter; + if (executable instanceof Method) { + methodParameter = new MethodParameter((Method) executable, i); + } else if (executable instanceof Constructor){ + methodParameter = new MethodParameter((Constructor) executable, i); + } else { + throw new IllegalArgumentException("Unsupported Executable: " + executable); + } + methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + return methodParameter; + } + +} diff --git a/src/main/java/org/springframework/shell2/commands/Console.java b/src/main/java/org/springframework/shell2/commands/Console.java new file mode 100644 index 00000000..832af911 --- /dev/null +++ b/src/main/java/org/springframework/shell2/commands/Console.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.commands; + +import org.jline.terminal.Terminal; +import org.jline.utils.InfoCmp; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.shell2.standard.ShellComponent; +import org.springframework.shell2.standard.ShellMethod; + +/** + * ANSI console related commands. + * + * @author Eric Bottard + */ +@ShellComponent +public class Console { + + @Autowired + private Terminal terminal; + + @ShellMethod(help = "Clear the shell screen.") + public void clear() { + terminal.puts(InfoCmp.Capability.clear_screen); + } +} diff --git a/src/main/java/org/springframework/shell2/commands/Help.java b/src/main/java/org/springframework/shell2/commands/Help.java index ac7d48d1..fc4adff9 100644 --- a/src/main/java/org/springframework/shell2/commands/Help.java +++ b/src/main/java/org/springframework/shell2/commands/Help.java @@ -1,218 +1,221 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.commands; - -import static java.util.stream.Collectors.mapping; -import static java.util.stream.Collectors.toCollection; - -import java.io.IOException; -import java.lang.reflect.Parameter; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; - -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.MethodParameter; -import org.springframework.shell2.MethodTarget; -import org.springframework.shell2.ParameterDescription; -import org.springframework.shell2.ParameterResolver; -import org.springframework.shell2.Shell; -import org.springframework.shell2.ShellComponent; -import org.springframework.shell2.ShellMethod; -import org.springframework.shell2.ShellOption; - -/** - * A command to display help about all available commands. - * - * @author Eric Bottard - */ -@ShellComponent -public class Help { - - private final List parameterResolvers; - - private Shell shell; - - @Autowired - public Help(List parameterResolvers) { - this.parameterResolvers = parameterResolvers; - } - - @Autowired // ctor injection impossible b/c of circular dependency - public void setShell(Shell shell) { - this.shell = shell; - } - - @ShellMethod(help = "Display help about available commands.", prefix = "-") - public CharSequence help( - @ShellOption(defaultValue = ShellOption.NULL, - value = {"C", "-command"}, - help = "The command to obtain help for.") String command) throws IOException { - if (command == null) { - return listCommands(); - } - else { - return documentCommand(command); - } - - } - - /** - * Return a description of a specific command. Uses a layout inspired by *nix man pages. - */ - private CharSequence documentCommand(String command) { - MethodTarget methodTarget = shell.listCommands().get(command); - if (methodTarget == null) { - throw new IllegalArgumentException("Unknown command '" + command + "'"); - } - - // NAME - AttributedStringBuilder result = new AttributedStringBuilder().append("\n\n"); - result.append("NAME", AttributedStyle.BOLD).append("\n\t"); - result.append(command).append(" - ").append(methodTarget.getHelp()).append("\n\n"); - - // SYNOPSYS - result.append("SYNOPSYS", AttributedStyle.BOLD).append("\n\t"); - result.append(command, AttributedStyle.BOLD); - result.append(" "); - - List parameterDescriptions = getParameterDescriptions(methodTarget); - - for (ParameterDescription description : parameterDescriptions) { - - if (description.defaultValue().isPresent()) { - result.append("["); - } - if (!description.mandatoryKey()) { - result.append("["); - } - result.append(description.keys().iterator().next(), AttributedStyle.BOLD); - if (!description.mandatoryKey()) { - result.append("]"); - if (!description.formal().isEmpty()) { - result.append(" "); - } - } - appendUnderlinedFormal(result, description); - if (description.defaultValue().isPresent()) { - result.append("]"); - } - result.append(" "); - } - result.append("\n\n"); - - // OPTIONS - result.append("OPTIONS", AttributedStyle.BOLD).append("\n"); - for (ParameterDescription description : parameterDescriptions) { - result.append("\t").append(description.keys().stream().collect(Collectors.joining(" or ")), AttributedStyle.BOLD); - if (description.formal().length() > 0) { - result.append(" "); - appendUnderlinedFormal(result, description); - result.append("\n\t"); - } - else if (description.keys().size() > 1) { - result.append("\n\t"); - } - result.append("\t"); - result.append(description.help()); - if (description.defaultValue().isPresent()) { - result - .append(" [Optional, default = ", AttributedStyle.BOLD) - .append(description.defaultValue().get(), AttributedStyle.BOLD.italic()) - .append("]", AttributedStyle.BOLD); - } else { - result.append(" [Mandatory]", AttributedStyle.BOLD); - } - result.append("\n\n"); - } - - // ALSO KNOWN AS - Set aliases = shell.listCommands().entrySet().stream() - .filter(e -> e.getValue().equals(methodTarget)) - .map(e -> e.getKey()) - .filter(c -> !command.equals(c)) - .collect(toCollection(TreeSet::new)); - - if (!aliases.isEmpty()) { - result.append("ALSO KNOWN AS", AttributedStyle.BOLD).append("\n"); - for (String alias : aliases) { - result.append('\t').append(alias).append('\n'); - } - } - - result.append("\n"); - return result; - } - - private CharSequence listCommands() { - Map> groupedByMethodTarget = shell.listCommands().entrySet().stream() - .collect(Collectors.groupingBy(e -> e.getValue().getHelp(), // Use help() as the grouping key - mapping(Map.Entry::getKey, toCollection(TreeSet::new)))); // accumulate the command 'names' into a sorted set - - // Then display commands, sorted alphabetically by their first alias - AttributedStringBuilder result = new AttributedStringBuilder(); - result.append("AVAILABLE COMMANDS\n\n", AttributedStyle.BOLD); - - groupedByMethodTarget.entrySet().stream() - .sorted(sortByFirstElement()) - .forEach(e -> result.append("\t") - .append(e.getValue().stream().collect(Collectors.joining(", ")), AttributedStyle.BOLD) - .append(": ") - .append(e.getKey()) - .append('\n') - ); - return result.append("\n"); - } - - private Comparator>> sortByFirstElement() { - return (e1, e2) -> e1.getValue().iterator().next().compareTo(e2.getValue().iterator().next()); - } - - private void appendUnderlinedFormal(AttributedStringBuilder result, ParameterDescription description) { - for (char c : description.formal().toCharArray()) { - if (c != ' ') { - result.append("" + c, AttributedStyle.DEFAULT.underline()); - } - else { - result.append(c); - } - } - } - - private List getParameterDescriptions(MethodTarget methodTarget) { - Parameter[] parameters = methodTarget.getMethod().getParameters(); - List parameterDescriptions = new ArrayList<>(); - for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) { - for (ParameterResolver parameterResolver : parameterResolvers) { - MethodParameter methodParameter = new MethodParameter(methodTarget.getMethod(), i); - if (parameterResolver.supports(methodParameter)) { - parameterDescriptions.add(parameterResolver.describe(methodParameter)); - break; - } - } - } - return parameterDescriptions; - } - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.commands; + +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toCollection; + +import java.io.IOException; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.shell2.MethodTarget; +import org.springframework.shell2.ParameterDescription; +import org.springframework.shell2.ParameterResolver; +import org.springframework.shell2.Shell; +import org.springframework.shell2.standard.ShellComponent; +import org.springframework.shell2.standard.ShellMethod; +import org.springframework.shell2.standard.ShellOption; +import org.springframework.shell2.Utils; + +/** + * A command to display help about all available commands. + * + * @author Eric Bottard + */ +@ShellComponent +public class Help { + + private final List parameterResolvers; + + private Shell shell; + + @Autowired + public Help(List parameterResolvers) { + this.parameterResolvers = parameterResolvers; + } + + @Autowired // ctor injection impossible b/c of circular dependency + public void setShell(Shell shell) { + this.shell = shell; + } + + @ShellMethod(help = "Display help about available commands.", prefix = "-") + public CharSequence help( + @ShellOption(defaultValue = ShellOption.NULL, + value = {"-C", "--command"}, + help = "The command to obtain help for.") String command) throws IOException { + if (command == null) { + return listCommands(); + } + else { + return documentCommand(command); + } + + } + + /** + * Return a description of a specific command. Uses a layout inspired by *nix man pages. + */ + private CharSequence documentCommand(String command) { + MethodTarget methodTarget = shell.listCommands().get(command); + if (methodTarget == null) { + throw new IllegalArgumentException("Unknown command '" + command + "'"); + } + + // NAME + AttributedStringBuilder result = new AttributedStringBuilder().append("\n\n"); + result.append("NAME", AttributedStyle.BOLD).append("\n\t"); + result.append(command).append(" - ").append(methodTarget.getHelp()).append("\n\n"); + + // SYNOPSYS + result.append("SYNOPSYS", AttributedStyle.BOLD).append("\n\t"); + result.append(command, AttributedStyle.BOLD); + result.append(" "); + + List parameterDescriptions = getParameterDescriptions(methodTarget); + + for (ParameterDescription description : parameterDescriptions) { + + if (description.defaultValue().isPresent()) { + result.append("["); + } + if (!description.mandatoryKey()) { + result.append("["); + } + result.append(description.keys().iterator().next(), AttributedStyle.BOLD); + if (!description.mandatoryKey()) { + result.append("]"); + if (!description.formal().isEmpty()) { + result.append(" "); + } + } + appendUnderlinedFormal(result, description); + if (description.defaultValue().isPresent()) { + result.append("]"); + } + result.append(" "); + } + result.append("\n\n"); + + // OPTIONS + if (!parameterDescriptions.isEmpty()) { + result.append("OPTIONS", AttributedStyle.BOLD).append("\n"); + } + for (ParameterDescription description : parameterDescriptions) { + result.append("\t").append(description.keys().stream().collect(Collectors.joining(" or ")), AttributedStyle.BOLD); + if (description.formal().length() > 0) { + result.append(" "); + appendUnderlinedFormal(result, description); + result.append("\n\t"); + } + else if (description.keys().size() > 1) { + result.append("\n\t"); + } + result.append("\t"); + result.append(description.help()); + if (description.defaultValue().isPresent()) { + result + .append(" [Optional, default = ", AttributedStyle.BOLD) + .append(description.defaultValue().get(), AttributedStyle.BOLD.italic()) + .append("]", AttributedStyle.BOLD); + } else { + result.append(" [Mandatory]", AttributedStyle.BOLD); + } + result.append("\n\n"); + } + + // ALSO KNOWN AS + Set aliases = shell.listCommands().entrySet().stream() + .filter(e -> e.getValue().equals(methodTarget)) + .map(e -> e.getKey()) + .filter(c -> !command.equals(c)) + .collect(toCollection(TreeSet::new)); + + if (!aliases.isEmpty()) { + result.append("ALSO KNOWN AS", AttributedStyle.BOLD).append("\n"); + for (String alias : aliases) { + result.append('\t').append(alias).append('\n'); + } + } + + result.append("\n"); + return result; + } + + private CharSequence listCommands() { + Map> groupedByMethodTarget = shell.listCommands().entrySet().stream() + .collect(Collectors.groupingBy(e -> e.getValue().getHelp(), // Use help() as the grouping key + mapping(Map.Entry::getKey, toCollection(TreeSet::new)))); // accumulate the command 'names' into a sorted set + + // Then display commands, sorted alphabetically by their first alias + AttributedStringBuilder result = new AttributedStringBuilder(); + result.append("AVAILABLE COMMANDS\n\n", AttributedStyle.BOLD); + + groupedByMethodTarget.entrySet().stream() + .sorted(sortByFirstElement()) + .forEach(e -> result.append("\t") + .append(e.getValue().stream().collect(Collectors.joining(", ")), AttributedStyle.BOLD) + .append(": ") + .append(e.getKey()) + .append('\n') + ); + return result.append("\n"); + } + + private Comparator>> sortByFirstElement() { + return (e1, e2) -> e1.getValue().iterator().next().compareTo(e2.getValue().iterator().next()); + } + + private void appendUnderlinedFormal(AttributedStringBuilder result, ParameterDescription description) { + for (char c : description.formal().toCharArray()) { + if (c != ' ') { + result.append("" + c, AttributedStyle.DEFAULT.underline()); + } + else { + result.append(c); + } + } + } + + private List getParameterDescriptions(MethodTarget methodTarget) { + Parameter[] parameters = methodTarget.getMethod().getParameters(); + List parameterDescriptions = new ArrayList<>(); + for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) { + for (ParameterResolver parameterResolver : parameterResolvers) { + MethodParameter methodParameter = Utils.createMethodParameter(methodTarget.getMethod(), i); + if (parameterResolver.supports(methodParameter)) { + parameterDescriptions.add(parameterResolver.describe(methodParameter)); + break; + } + } + } + return parameterDescriptions; + } + +} diff --git a/src/main/java/org/springframework/shell2/commands/Quit.java b/src/main/java/org/springframework/shell2/commands/Quit.java index 272ddec2..41927158 100644 --- a/src/main/java/org/springframework/shell2/commands/Quit.java +++ b/src/main/java/org/springframework/shell2/commands/Quit.java @@ -1,35 +1,35 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.commands; - -import org.springframework.shell2.ExitRequest; -import org.springframework.shell2.ShellComponent; -import org.springframework.shell2.ShellMethod; - -/** - * A command that terminates the running shell. - * - * @author Eric Bottard - */ -@ShellComponent("") -public class Quit { - - @ShellMethod(help = "Exit the shell") - public void quit() { - throw new ExitRequest(); - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.commands; + +import org.springframework.shell2.ExitRequest; +import org.springframework.shell2.standard.ShellComponent; +import org.springframework.shell2.standard.ShellMethod; + +/** + * A command that terminates the running shell. + * + * @author Eric Bottard + */ +@ShellComponent +public class Quit { + + @ShellMethod(help = "Exit the shell.", value = {"quit", "exit"}) + public void quit() { + throw new ExitRequest(); + } +} diff --git a/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java b/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java index ac29542f..db62899f 100644 --- a/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java +++ b/src/main/java/org/springframework/shell2/jcommander/JCommanderParameterResolver.java @@ -1,84 +1,91 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.jcommander; - -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import com.beust.jcommander.DynamicParameter; -import com.beust.jcommander.JCommander; -import com.beust.jcommander.Parameter; -import com.beust.jcommander.ParametersDelegate; - -import org.springframework.beans.BeanUtils; -import org.springframework.core.MethodParameter; -import org.springframework.shell2.ParameterDescription; -import org.springframework.shell2.ParameterResolver; -import org.springframework.util.ReflectionUtils; - -/** - * Created by ericbottard on 15/12/15. - */ -public class JCommanderParameterResolver implements ParameterResolver { - - private static final Collection> JCOMMANDER_ANNOTATIONS = - Arrays.asList(Parameter.class, DynamicParameter.class, ParametersDelegate.class); - - @Override - public boolean supports(MethodParameter parameter) { - AtomicBoolean isSupported = new AtomicBoolean(false); - - Class parameterType = parameter.getParameterType(); - ReflectionUtils.doWithFields(parameterType, field -> { - ReflectionUtils.makeAccessible(field); - boolean hasAnnotation = Arrays.asList(field.getAnnotations()) - .stream() - .map(Annotation::annotationType) - .anyMatch(JCOMMANDER_ANNOTATIONS::contains); - isSupported.compareAndSet(false, hasAnnotation); - - }); - - ReflectionUtils.doWithMethods(parameterType, method -> { - ReflectionUtils.makeAccessible(method); - boolean hasAnnotation = Arrays.asList(method.getAnnotations()) - .stream() - .map(Annotation::annotationType) - .anyMatch(Parameter.class::equals); - isSupported.compareAndSet(false, hasAnnotation); - }); - return isSupported.get(); - } - - @Override - public Object resolve(MethodParameter methodParameter, List words) { - Object pojo = BeanUtils.instantiateClass(methodParameter.getParameterType()); - JCommander jCommander = new JCommander(); - jCommander.addObject(pojo); - jCommander.setAcceptUnknownOptions(true); - jCommander.parse(words.toArray(new String[words.size()])); - return pojo; - } - - @Override - public ParameterDescription describe(MethodParameter parameter) { - throw new UnsupportedOperationException(); - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.jcommander; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.beust.jcommander.DynamicParameter; +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParametersDelegate; + +import org.springframework.beans.BeanUtils; +import org.springframework.core.MethodParameter; +import org.springframework.shell2.CompletionContext; +import org.springframework.shell2.CompletionProposal; +import org.springframework.shell2.ParameterDescription; +import org.springframework.shell2.ParameterResolver; +import org.springframework.util.ReflectionUtils; + +/** + * Created by ericbottard on 15/12/15. + */ +public class JCommanderParameterResolver implements ParameterResolver { + + private static final Collection> JCOMMANDER_ANNOTATIONS = + Arrays.asList(Parameter.class, DynamicParameter.class, ParametersDelegate.class); + + @Override + public boolean supports(MethodParameter parameter) { + AtomicBoolean isSupported = new AtomicBoolean(false); + + Class parameterType = parameter.getParameterType(); + ReflectionUtils.doWithFields(parameterType, field -> { + ReflectionUtils.makeAccessible(field); + boolean hasAnnotation = Arrays.asList(field.getAnnotations()) + .stream() + .map(Annotation::annotationType) + .anyMatch(JCOMMANDER_ANNOTATIONS::contains); + isSupported.compareAndSet(false, hasAnnotation); + + }); + + ReflectionUtils.doWithMethods(parameterType, method -> { + ReflectionUtils.makeAccessible(method); + boolean hasAnnotation = Arrays.asList(method.getAnnotations()) + .stream() + .map(Annotation::annotationType) + .anyMatch(Parameter.class::equals); + isSupported.compareAndSet(false, hasAnnotation); + }); + return isSupported.get(); + } + + @Override + public Object resolve(MethodParameter methodParameter, List words) { + Object pojo = BeanUtils.instantiateClass(methodParameter.getParameterType()); + JCommander jCommander = new JCommander(); + jCommander.addObject(pojo); + jCommander.setAcceptUnknownOptions(true); + jCommander.parse(words.toArray(new String[words.size()])); + return pojo; + } + + @Override + public ParameterDescription describe(MethodParameter parameter) { + throw new UnsupportedOperationException(); + } + + @Override + public List complete(MethodParameter parameter, CompletionContext context) { + return null; + } +} diff --git a/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java b/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java index 03d9708c..2811cb77 100644 --- a/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java +++ b/src/main/java/org/springframework/shell2/legacy/LegacyMethodTargetResolver.java @@ -1,54 +1,54 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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; - -/** - * A {@link MethodTargetResolver} that discovers methods annotated with {@link CliCommand} on beans - * implementing the {@link CommandMarker} marker interface. - * @author Eric Bottard - * @author Florent Biville - */ -@Component -public class LegacyMethodTargetResolver implements MethodTargetResolver { - - @Override - public Map resolve(ApplicationContext context) { - Map methodTargets = new HashMap<>(); - Map beans = context.getBeansOfType(CommandMarker.class); - for (Object bean : beans.values()) { - Class clazz = bean.getClass(); - ReflectionUtils.doWithMethods(clazz, method -> { - CliCommand cliCommand = method.getAnnotation(CliCommand.class); - for (String key : cliCommand.value()) { - methodTargets.put(key, new MethodTarget(method, bean, cliCommand.help())); - } - }, method -> method.getAnnotation(CliCommand.class) != null); - } - return methodTargets; - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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; + +/** + * A {@link MethodTargetResolver} that discovers methods annotated with {@link CliCommand} on beans + * implementing the {@link CommandMarker} marker interface. + * @author Eric Bottard + * @author Florent Biville + */ +@Component +public class LegacyMethodTargetResolver implements MethodTargetResolver { + + @Override + public Map resolve(ApplicationContext context) { + Map methodTargets = new HashMap<>(); + Map beans = context.getBeansOfType(CommandMarker.class); + for (Object bean : beans.values()) { + Class clazz = bean.getClass(); + ReflectionUtils.doWithMethods(clazz, method -> { + CliCommand cliCommand = method.getAnnotation(CliCommand.class); + for (String key : cliCommand.value()) { + methodTargets.put(key, new MethodTarget(method, bean, cliCommand.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 index 9516c8fb..a37830cf 100644 --- a/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java +++ b/src/main/java/org/springframework/shell2/legacy/LegacyParameterResolver.java @@ -1,132 +1,139 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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.ParameterDescription; -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 { - - @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())); - } - } - - @Override - public ParameterDescription describe(MethodParameter parameter) { - throw new UnsupportedOperationException(); - } - - 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); - } - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.CompletionContext; +import org.springframework.shell2.CompletionProposal; +import org.springframework.shell2.ParameterDescription; +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 { + + @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())); + } + } + + @Override + public ParameterDescription describe(MethodParameter parameter) { + throw new UnsupportedOperationException(); + } + + @Override + public List complete(MethodParameter parameter, CompletionContext context) { + return null; + } + + 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/main/java/org/springframework/shell2/result/AttributedCharSequenceResultHandler.java b/src/main/java/org/springframework/shell2/result/AttributedCharSequenceResultHandler.java index 00368af8..550fd1d3 100644 --- a/src/main/java/org/springframework/shell2/result/AttributedCharSequenceResultHandler.java +++ b/src/main/java/org/springframework/shell2/result/AttributedCharSequenceResultHandler.java @@ -1,44 +1,44 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.result; - -import org.jline.terminal.Terminal; -import org.jline.utils.AttributedCharSequence; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -/** - * A {@link ResultHandler} that knows how to render JLine's {@link AttributedCharSequence}. - * - * @author Eric Bottard - */ -@Component -public class AttributedCharSequenceResultHandler implements ResultHandler { - - private final Terminal terminal; - - @Autowired - public AttributedCharSequenceResultHandler(Terminal terminal) { - this.terminal = terminal; - } - - @Override - public void handleResult(AttributedCharSequence result) { - System.out.println(result.toAnsi(terminal)); - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.result; + +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedCharSequence; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * A {@link ResultHandler} that knows how to render JLine's {@link AttributedCharSequence}. + * + * @author Eric Bottard + */ +@Component +public class AttributedCharSequenceResultHandler implements ResultHandler { + + private final Terminal terminal; + + @Autowired + public AttributedCharSequenceResultHandler(Terminal terminal) { + this.terminal = terminal; + } + + @Override + public void handleResult(AttributedCharSequence result) { + System.out.println(result.toAnsi(terminal)); + } +} diff --git a/src/main/java/org/springframework/shell2/result/DefaultResultHandler.java b/src/main/java/org/springframework/shell2/result/DefaultResultHandler.java index ce21704f..799bd849 100644 --- a/src/main/java/org/springframework/shell2/result/DefaultResultHandler.java +++ b/src/main/java/org/springframework/shell2/result/DefaultResultHandler.java @@ -1,34 +1,34 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.result; - -import org.springframework.stereotype.Component; - -/** - * A simple {@link ResultHandler} that deals with Objects (hence comes as a last resort) - * and prints the {@link Object#toString()} value of results to standard out. - * - * @author Eric Bottard - */ -@Component -public class DefaultResultHandler implements ResultHandler { - - @Override - public void handleResult(Object result) { - System.out.println(String.valueOf(result)); - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.result; + +import org.springframework.stereotype.Component; + +/** + * A simple {@link ResultHandler} that deals with Objects (hence comes as a last resort) + * and prints the {@link Object#toString()} value of results to standard out. + * + * @author Eric Bottard + */ +@Component +public class DefaultResultHandler implements ResultHandler { + + @Override + public void handleResult(Object result) { + System.out.println(String.valueOf(result)); + } +} diff --git a/src/main/java/org/springframework/shell2/result/IterableResultHandler.java b/src/main/java/org/springframework/shell2/result/IterableResultHandler.java index de624a3a..86e85ddd 100644 --- a/src/main/java/org/springframework/shell2/result/IterableResultHandler.java +++ b/src/main/java/org/springframework/shell2/result/IterableResultHandler.java @@ -1,44 +1,44 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.result; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -/** - * A {@link ResultHandler} that deals with {@link Iterable}s and delegates to - * {@link ResultHandlers} for each element in turn. - * - * @author Eric Bottard - */ -@Component -public class IterableResultHandler implements ResultHandler { - - private ResultHandlers resultHandlers; - - @Autowired - public void setResultHandlers(ResultHandlers resultHandlers) { - this.resultHandlers = resultHandlers; - } - - @Override - public void handleResult(Iterable result) { - for (Object o : result) { - resultHandlers.handleResult(o); - } - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.result; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * A {@link ResultHandler} that deals with {@link Iterable}s and delegates to + * {@link ResultHandlers} for each element in turn. + * + * @author Eric Bottard + */ +@Component +public class IterableResultHandler implements ResultHandler { + + private ResultHandlers resultHandlers; + + @Autowired + public void setResultHandlers(ResultHandlers resultHandlers) { + this.resultHandlers = resultHandlers; + } + + @Override + public void handleResult(Iterable result) { + for (Object o : result) { + resultHandlers.handleResult(o); + } + } +} diff --git a/src/main/java/org/springframework/shell2/result/ResultHandler.java b/src/main/java/org/springframework/shell2/result/ResultHandler.java index 100643f1..357c4584 100644 --- a/src/main/java/org/springframework/shell2/result/ResultHandler.java +++ b/src/main/java/org/springframework/shell2/result/ResultHandler.java @@ -1,32 +1,32 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.result; - -/** - * Implementations know how to deal with results of method invocations, whether normal results or exceptions thrown. - * - * @author Eric Bottard - */ -public interface ResultHandler { - - /** - * Deal with some method execution result, whether it was the normal return value, or some kind - * of {@link Throwable}. - */ - void handleResult(T result); - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.result; + +/** + * Implementations know how to deal with results of method invocations, whether normal results or exceptions thrown. + * + * @author Eric Bottard + */ +public interface ResultHandler { + + /** + * Deal with some method execution result, whether it was the normal return value, or some kind + * of {@link Throwable}. + */ + void handleResult(T result); + +} diff --git a/src/main/java/org/springframework/shell2/result/ResultHandlers.java b/src/main/java/org/springframework/shell2/result/ResultHandlers.java index 5311bdb0..3c1a53f9 100644 --- a/src/main/java/org/springframework/shell2/result/ResultHandlers.java +++ b/src/main/java/org/springframework/shell2/result/ResultHandlers.java @@ -1,73 +1,73 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.result; - -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -/** - * A unique entry point delegating to the most appropriate {@link ResultHandler}, - * according to the type of result to handle. - * - * @author Eric Bottard - */ -@Component -public class ResultHandlers { - - private Map, ResultHandler> resultHandlers = new HashMap<>(); - - @SuppressWarnings("unchecked") - public void handleResult(Object result) { - if (result == null) { // void methods - return; - } - Class clazz = result.getClass(); - ResultHandler handler = getResultHandler(clazz); - handler.handleResult(result); - } - - private ResultHandler getResultHandler(Class clazz) { - ResultHandler handler = resultHandlers.get(clazz); - if (handler != null) { - return handler; - } - else { - for (Class type : clazz.getInterfaces()) { - handler = getResultHandler(type); - if (handler != null) { - return handler; - } - } - return clazz.getSuperclass() != null ? getResultHandler(clazz.getSuperclass()) : null; - } - } - - @Autowired - public void setResultHandlers(Set> resultHandlers) { - for (ResultHandler resultHandler : resultHandlers) { - Type type = ((ParameterizedType) resultHandler.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]; - this.resultHandlers.put((Class) type, resultHandler); - } - } - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.result; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * A unique entry point delegating to the most appropriate {@link ResultHandler}, + * according to the type of result to handle. + * + * @author Eric Bottard + */ +@Component +public class ResultHandlers { + + private Map, ResultHandler> resultHandlers = new HashMap<>(); + + @SuppressWarnings("unchecked") + public void handleResult(Object result) { + if (result == null) { // void methods + return; + } + Class clazz = result.getClass(); + ResultHandler handler = getResultHandler(clazz); + handler.handleResult(result); + } + + private ResultHandler getResultHandler(Class clazz) { + ResultHandler handler = resultHandlers.get(clazz); + if (handler != null) { + return handler; + } + else { + for (Class type : clazz.getInterfaces()) { + handler = getResultHandler(type); + if (handler != null) { + return handler; + } + } + return clazz.getSuperclass() != null ? getResultHandler(clazz.getSuperclass()) : null; + } + } + + @Autowired + public void setResultHandlers(Set> resultHandlers) { + for (ResultHandler resultHandler : resultHandlers) { + Type type = ((ParameterizedType) resultHandler.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]; + this.resultHandlers.put((Class) type, resultHandler); + } + } + +} diff --git a/src/main/java/org/springframework/shell2/result/ThrowableResultHandler.java b/src/main/java/org/springframework/shell2/result/ThrowableResultHandler.java index 6cfa1e6a..8ebaa86d 100644 --- a/src/main/java/org/springframework/shell2/result/ThrowableResultHandler.java +++ b/src/main/java/org/springframework/shell2/result/ThrowableResultHandler.java @@ -1,37 +1,37 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.result; - -import org.jline.utils.AttributedString; -import org.jline.utils.AttributedStyle; - -import org.springframework.stereotype.Component; - -/** - * A {@link ResultHandler} that prints thrown exceptions messages in red. - * - * @author Eric Bottard - */ -@Component -public class ThrowableResultHandler implements ResultHandler { - - @Override - public void handleResult(Throwable result) { - System.out.println(new AttributedString(result.getMessage(), - AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi()); - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.result; + +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStyle; + +import org.springframework.stereotype.Component; + +/** + * A {@link ResultHandler} that prints thrown exceptions messages in red. + * + * @author Eric Bottard + */ +@Component +public class ThrowableResultHandler implements ResultHandler { + + @Override + public void handleResult(Throwable result) { + System.out.println(new AttributedString(result.toString(), + AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi()); + } +} diff --git a/src/main/java/org/springframework/shell2/standard/EnumValueProvider.java b/src/main/java/org/springframework/shell2/standard/EnumValueProvider.java new file mode 100644 index 00000000..1bf45c24 --- /dev/null +++ b/src/main/java/org/springframework/shell2/standard/EnumValueProvider.java @@ -0,0 +1,52 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.shell2.CompletionContext; +import org.springframework.shell2.CompletionProposal; + +/** + * A {@link ValueProvider} that knows how to complete values for {@link Enum} typed parameters. + * @author Eric Bottard + */ +public class EnumValueProvider implements ValueProvider { + + @Override + public boolean supports(MethodParameter parameter, CompletionContext completionContext) { + return Enum.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public List complete(MethodParameter parameter, CompletionContext completionContext, String[] hints) { + List result = new ArrayList<>(); + for (Object v : parameter.getParameterType().getEnumConstants()) { + Enum e = (Enum) v; + String prefix = completionContext.currentWordUpToCursor(); + if (prefix == null) { + prefix = ""; + } + if (e.name().startsWith(prefix)) { + result.add(new CompletionProposal(e.name())); + } + } + return result; + } +} diff --git a/src/main/java/org/springframework/shell2/ShellComponent.java b/src/main/java/org/springframework/shell2/standard/ShellComponent.java similarity index 94% rename from src/main/java/org/springframework/shell2/ShellComponent.java rename to src/main/java/org/springframework/shell2/standard/ShellComponent.java index 6184f773..4231985a 100644 --- a/src/main/java/org/springframework/shell2/ShellComponent.java +++ b/src/main/java/org/springframework/shell2/standard/ShellComponent.java @@ -1,46 +1,47 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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; - -/** - * Indicates that an annotated class may contain shell methods (themselves annotated with {@link @ShellMethod}) that - * is, - * methods that may be invoked reflectively by the shell. - * - *

This annotation is a specialization of {@link Component}.

- * @author Eric Bottard - * @see Component - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -@Documented -@Component -public @interface ShellComponent { - - /** - * Used to indicate a suggestion for a logical name for the component. - */ - String value() default ""; -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +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; + +/** + * Indicates that an annotated class may contain shell methods (themselves annotated with {@link @ShellMethod}) that + * is, + * methods that may be invoked reflectively by the shell. + * + *

This annotation is a specialization of {@link Component}.

+ * + * @author Eric Bottard + * @see Component + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Component +public @interface ShellComponent { + + /** + * Used to indicate a suggestion for a logical name for the component. + */ + String value() default ""; +} diff --git a/src/main/java/org/springframework/shell2/ShellMethod.java b/src/main/java/org/springframework/shell2/standard/ShellMethod.java similarity index 94% rename from src/main/java/org/springframework/shell2/ShellMethod.java rename to src/main/java/org/springframework/shell2/standard/ShellMethod.java index 1647ff04..30e0bde9 100644 --- a/src/main/java/org/springframework/shell2/ShellMethod.java +++ b/src/main/java/org/springframework/shell2/standard/ShellMethod.java @@ -1,52 +1,53 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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; - -/** - * Used to mark a method as invokable via Spring Shell. - * @author Eric Bottard - * @author Florent Biville - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) -@Documented -public @interface ShellMethod { - - /** - * The name(s) by which this method can be invoked via Spring Shell. If not specified, the actual method name - * will be used (turning camelCase humps into "-"). - */ - String[] value() default {}; - - /** - * A description for the command. Should not contain any formatting (e.g. html) characters and would typically - * start with a capital letter and end with a dot. - */ - String help() default ""; - - /** - * The prefix to use for assigning parameters by name. - */ - String prefix() default "--"; - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +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; + +/** + * Used to mark a method as invokable via Spring Shell. + * + * @author Eric Bottard + * @author Florent Biville + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Documented +public @interface ShellMethod { + + /** + * The name(s) by which this method can be invoked via Spring Shell. If not specified, the actual method name + * will be used (turning camelCase humps into "-"). + */ + String[] value() default {}; + + /** + * A description for the command. Should not contain any formatting (e.g. html) characters and would typically + * start with a capital letter and end with a dot. + */ + String help() default ""; + + /** + * The prefix to use for assigning parameters by name. + */ + String prefix() default "--"; + +} diff --git a/src/main/java/org/springframework/shell2/ShellOption.java b/src/main/java/org/springframework/shell2/standard/ShellOption.java similarity index 87% rename from src/main/java/org/springframework/shell2/ShellOption.java rename to src/main/java/org/springframework/shell2/standard/ShellOption.java index a0bc14e7..62dea8c9 100644 --- a/src/main/java/org/springframework/shell2/ShellOption.java +++ b/src/main/java/org/springframework/shell2/standard/ShellOption.java @@ -1,57 +1,66 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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; - -/** - * Used to customize handling of a {@link ShellMethod} parameters. - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) -public @interface ShellOption { - - String NULL = "__NULL__"; - - String NONE = "__NONE__"; - - /** - * The key(s) (without the {@link ShellMethod#prefix()}) by which this parameter can be referenced - * when using named parameters. If none is specified, the actual method parameter name will be used. - */ - String[] value() default {}; - - /** - * Return the number of input "words" this parameter consumes. - */ - int arity() default 1; - - /** - * The textual (pre-conversion) value to assign to this parameter if no value is provided by the user. - */ - String defaultValue() default NONE; - - /** - * Return a short description of the parameter. - */ - String help() default ""; -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +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; + +/** + * Used to customize handling of a {@link ShellMethod} parameter. + * + * @author Eric Bottard + * @author Florent Biville + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ShellOption { + + String NULL = "__NULL__"; + + String NONE = "__NONE__"; + + /** + * The key(s) (without the {@link ShellMethod#prefix()}) by which this parameter can be referenced + * when using named parameters. If none is specified, the actual method parameter name will be used. + */ + String[] value() default {}; + + /** + * Return the number of input "words" this parameter consumes. + */ + int arity() default 1; + + /** + * The textual (pre-conversion) value to assign to this parameter if no value is provided by the user. + */ + String defaultValue() default NONE; + + /** + * Return a short description of the parameter. + */ + String help() default ""; + + Class valueProvider() default NoValueProvider.class; + + interface NoValueProvider extends ValueProvider { + + } +} diff --git a/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java b/src/main/java/org/springframework/shell2/standard/StandardMethodTargetResolver.java similarity index 78% rename from src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java rename to src/main/java/org/springframework/shell2/standard/StandardMethodTargetResolver.java index ba520d9d..64985358 100644 --- a/src/main/java/org/springframework/shell2/DefaultMethodTargetResolver.java +++ b/src/main/java/org/springframework/shell2/standard/StandardMethodTargetResolver.java @@ -1,51 +1,57 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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 -> { - ShellMethod shellMapping = method.getAnnotation(ShellMethod.class); - String[] keys = shellMapping.value(); - if (keys.length == 0) { - keys = new String[] {method.getName()}; - } - for (String key : keys) { - methodTargets.put(key, new MethodTarget(method, bean, shellMapping.help())); - } - }, method -> method.getAnnotation(ShellMethod.class) != null); - } - return methodTargets; - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.context.ApplicationContext; +import org.springframework.shell2.MethodTarget; +import org.springframework.shell2.MethodTargetResolver; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; + +/** + * The standard implementation of {@link MethodTargetResolver} for new shell applications, + * resolves methods annotated with {@link ShellMethod} on {@link ShellComponent} beans. + * + * @author Eric Bottard + * @author Florent Biville + */ +@Component +public class StandardMethodTargetResolver 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 -> { + ShellMethod shellMapping = method.getAnnotation(ShellMethod.class); + String[] keys = shellMapping.value(); + if (keys.length == 0) { + keys = new String[] {method.getName()}; + } + for (String key : keys) { + methodTargets.put(key, new MethodTarget(method, bean, shellMapping.help())); + } + }, method -> method.getAnnotation(ShellMethod.class) != null); + } + return methodTargets; + } +} diff --git a/src/main/java/org/springframework/shell2/DefaultParameterResolver.java b/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java similarity index 55% rename from src/main/java/org/springframework/shell2/DefaultParameterResolver.java rename to src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java index 4e776d0e..bbd1c296 100644 --- a/src/main/java/org/springframework/shell2/DefaultParameterResolver.java +++ b/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java @@ -1,307 +1,458 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -import static org.springframework.shell2.Utils.unCamelify; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.MethodParameter; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; - -/** - * Default ParameterResolver implementation that supports the following features:
    - *
  • named parameters (recognized because they start with some {@link ShellMethod#prefix()}
  • - *
  • implicit named parameters (from the actual method parameter name)
  • - *
  • positional parameters (in order, for all parameter values that were not resolved via named parameters)
  • - *
  • default values (for all remaining parameters)
  • - *
- * - *

Method arguments can consume several words of input at once (driven by {@link ShellOption#arity()}, default 1). - * If several words are consumed, they will be joined together as a comma separated value and passed to the {@link - * ConversionService} - * (which will typically return a List or array).

- * - *

Boolean parameters are by default expected to have an arity of 0, allowing invocations in the form {@code rm - * --force --dir /foo}: - * the presence of {@code --force} passes {@code true} as a parameter value, while its absence passes {@code false}. - * Both - * the default arity of 0 and the default value of {@code false} can be overridden via {@link ShellOption} - * if needed.

- * @author Eric Bottard - * @author Florent Biville - */ -public class DefaultParameterResolver implements ParameterResolver { - - private final ConversionService conversionService; - - /** - * A cache from method+input to String representation of actual parameter values. - * Note that the converted result is not cached, to allow dynamic computation to happen at every invocation - * if needed (e.g. if a remote service is involved). - */ - private final Map> parameterCache = new ConcurrentReferenceHashMap<>(); - - public DefaultParameterResolver(ConversionService conversionService) { - this.conversionService = conversionService; - } - - @Override - public boolean supports(MethodParameter parameter) { - return true; - } - - @Override - public Object resolve(MethodParameter methodParameter, List words) { - String prefix = prefixForMethod(methodParameter); - - CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), words); - Map resolved = parameterCache.computeIfAbsent(cacheKey, (k) -> { - - Map result = new HashMap<>(); - Map namedParameters = new HashMap<>(); - List positionalValues = new ArrayList<>(); - - // First, resolve all parameters passed by-name - for (int i = 0; i < words.size(); i++) { - String word = words.get(i); - if (word.startsWith(prefix)) { - String key = word.substring(prefix.length()); - Parameter parameter = lookupParameterForKey(methodParameter.getMethod(), key, prefix); - int arity = getArity(parameter); - - String raw = words.subList(i + 1, i + 1 + arity).stream().collect(Collectors.joining(",")); - Assert.isTrue(!namedParameters.containsKey(key), String.format("Parameter for '%s' has already been specified", word)); - namedParameters.put(key, raw); - result.put(parameter, raw); - i += arity; - if (arity == 0) { - boolean defaultValue = booleanDefaultValue(parameter); - // Boolean parameter has been specified. Use the opposite of the default value - result.put(parameter, String.valueOf(!defaultValue)); - } - } // store for later processing of positional params - else { - positionalValues.add(word); - } - } - - // Now have a second pass over params and treat them as positional - int offset = 0; - Parameter[] parameters = methodParameter.getMethod().getParameters(); - for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) { - Parameter parameter = parameters[i]; - // Compute the intersection between possible keys for the param and what we've already seen for named params - Collection keys = getKeysForParameter(methodParameter.getMethod(), i); - Collection copy = new HashSet<>(keys); - copy.retainAll(namedParameters.keySet()); - if (copy.isEmpty()) { // Was not set via a key (including aliases), must be positional - int arity = getArity(parameter); - if (offset < positionalValues.size() && (offset + arity) <= positionalValues.size()) { - String raw = positionalValues.subList(offset, offset + arity).stream().collect(Collectors.joining(",")); - result.put(parameter, raw); - offset += arity; - } // No more input. Try defaultValues - else { - Optional defaultValue = defaultValueFor(parameter); - String value = defaultValue.orElseThrow(() -> new RuntimeException(String.format("Ran out of input for " + keys))); - result.put(parameter, value); - } - } - else if (copy.size() > 1) { - throw new IllegalArgumentException("Named parameter has been specified multiple times via " + prefix(copy, prefix)); - } - } - - Assert.isTrue(offset == positionalValues.size(), "Too many arguments: the following could not be mapped to parameters: " - + positionalValues.subList(offset, positionalValues.size()).stream().collect(Collectors.joining(" ", "'", "'"))); - return result; - }); - - String s = resolved.get(methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()]); - if (ShellOption.NULL.equals(s)) { - return null; - } - else { - return conversionService.convert(s, TypeDescriptor.valueOf(String.class), new TypeDescriptor(methodParameter)); - } - } - - private String prefixForMethod(MethodParameter methodParameter) { - return methodParameter.getMethod().getAnnotation(ShellMethod.class).prefix(); - } - - private Optional defaultValueFor(Parameter parameter) { - Optional defaultValue = Optional.empty(); - ShellOption option = parameter.getAnnotation(ShellOption.class); - if (option != null && !ShellOption.NONE.equals(option.defaultValue())) { - defaultValue = Optional.of(option.defaultValue()); - } - return defaultValue; - } - - @Override - public ParameterDescription describe(MethodParameter parameter) { - Parameter jlrParameter = parameter.getMethod().getParameters()[parameter.getParameterIndex()]; - int arity = getArity(jlrParameter); - Class type = parameter.getParameterType(); - ShellOption option = jlrParameter.getAnnotation(ShellOption.class); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < arity; i++) { - if (i > 0) { - sb.append(" "); - } - sb.append(arity > 1 ? unCamelify(removeMultiplicityFromType(parameter).getSimpleName()) : unCamelify(type.getSimpleName())); - } - ParameterDescription result = ParameterDescription.ofType(type); - result.formal(sb.toString()); - if (option != null) { - result.help(option.help()); - Optional defaultValue = defaultValueFor(jlrParameter); - if (defaultValue.isPresent()) { - result.defaultValue(defaultValue.map(dv -> dv.equals(ShellOption.NULL) ? "" : dv).get()); - } - } - List rawKeys = getKeysForParameter(parameter.getMethod(), parameter.getParameterIndex()); - String prefix = prefixForMethod(parameter); - result - .keys(rawKeys.stream().map(k -> prefix + k).collect(Collectors.toList())) - .mandatoryKey(false); - - return result; - } - - /** - * In case of {@code foo[] or Collection} and arity > 1, return the element type. - */ - private Class removeMultiplicityFromType(MethodParameter parameter) { - Class parameterType = parameter.getParameterType(); - if (parameterType.isArray()) { - return parameterType.getComponentType(); - } - else if (parameterType.isAssignableFrom(Collection.class)) { - return parameter.getNestedParameterType(); - } - else { - throw new RuntimeException("For " + parameter + " (with arity > 1) expected an array/collection type"); - } - } - - /** - * Add the command prefix back to the list of keys that was used to invoke the method. - */ - private String prefix(Collection keys, String prefix) { - return keys.stream().map(k -> prefix + k).collect(Collectors.joining(", ", "'", "'")); - } - - private boolean booleanDefaultValue(Parameter parameter) { - ShellOption option = parameter.getAnnotation(ShellOption.class); - if (option != null && !ShellOption.NULL.equals(option.defaultValue())) { - return Boolean.parseBoolean(option.defaultValue()); - } - return false; - } - - /** - * Return the arity of a given parameter. The default arity is 1, except for - * booleans where arity is 0 (can be overridden back to 1 via an annotation) - */ - private int getArity(Parameter parameter) { - ShellOption option = parameter.getAnnotation(ShellOption.class); - int inferred = (parameter.getType() == boolean.class || parameter.getType() == Boolean.class) ? 0 : 1; - return option != null ? option.arity() : inferred; - } - - /** - * Return the key(s) the i-th parameter of the command method, resolved either from the {@link ShellOption} - * annotation, - * or from the actual parameter name. - * @throws IllegalArgumentException if parameter names could not be extracted - */ - private List getKeysForParameter(Method method, int index) { - Parameter parameter = method.getParameters()[index]; - ShellOption option = parameter.getAnnotation(ShellOption.class); - if (option != null && option.value().length > 0) { - return Arrays.asList(option.value()); - } - else { - MethodParameter methodParameter = new MethodParameter(method, index); - methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - String parameterName = methodParameter.getParameterName(); - Assert.notNull(parameterName, String.format( - "Could not discover parameter name at index %d for %s, and option key(s) were not specified via %s annotation", - index, method, ShellOption.class.getSimpleName())); - return Collections.singletonList(parameterName); - } - } - - /** - * Return the method parameter that should be bound to the given key. - */ - private Parameter lookupParameterForKey(Method method, String key, String prefix) { - Parameter[] parameters = method.getParameters(); - for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) { - Parameter p = parameters[i]; - if (getKeysForParameter(method, i).contains(key)) { - return p; - } - } - throw new IllegalArgumentException(String.format("Could not look up parameter for '%s%s' in %s", prefix, key, method)); - } - - private static class CacheKey { - - private final Method method; - - private final List words; - - private CacheKey(Method method, List words) { - this.method = method; - this.words = words; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CacheKey cacheKey = (CacheKey) o; - return Objects.equals(method, cacheKey.method) && - Objects.equals(words, cacheKey.words); - } - - @Override - public int hashCode() { - return Objects.hash(method, words); - } - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +import static org.springframework.shell2.Utils.unCamelify; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.shell2.CompletionContext; +import org.springframework.shell2.CompletionProposal; +import org.springframework.shell2.ParameterDescription; +import org.springframework.shell2.ParameterMissingResolutionException; +import org.springframework.shell2.ParameterResolver; +import org.springframework.shell2.UnfinishedParameterResolutionException; +import org.springframework.shell2.Utils; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Default ParameterResolver implementation that supports the following features:
    + *
  • named parameters (recognized because they start with some {@link ShellMethod#prefix()})
  • + *
  • implicit named parameters (from the actual method parameter name)
  • + *
  • positional parameters (in order, for all parameter values that were not resolved via named + * parameters)
  • + *
  • default values (for all remaining parameters)
  • + *
+ * + *

Method arguments can consume several words of input at once (driven by {@link ShellOption#arity()}, default 1). + * If several words are consumed, they will be joined together as a comma separated value and passed to the {@link + * ConversionService} + * (which will typically return a List or array).

+ * + *

Boolean parameters are by default expected to have an arity of 0, allowing invocations in the form {@code rm + * --force --dir /foo}: + * the presence of {@code --force} passes {@code true} as a parameter value, while its absence passes {@code false}. + * Both + * the default arity of 0 and the default value of {@code false} can be overridden via {@link ShellOption} + * if needed.

+ * @author Eric Bottard + * @author Florent Biville + */ +public class StandardParameterResolver implements ParameterResolver { + + private final ConversionService conversionService; + + private Collection valueProviders = new HashSet<>(); + + /** + * A cache from method+input to String representation of actual parameter values. + * Note that the converted result is not cached, to allow dynamic computation to happen at every invocation + * if needed (e.g. if a remote service is involved). + */ + private final Map> parameterCache = new ConcurrentReferenceHashMap<>(); + + public StandardParameterResolver(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Autowired(required = false) + public void setValueProviders(Collection valueProviders) { + this.valueProviders = valueProviders; + } + + @Override + public boolean supports(MethodParameter parameter) { + return true; + } + + @Override + public Object resolve(MethodParameter methodParameter, List words) { + String prefix = prefixForMethod(methodParameter); + + CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), words); + Map resolved = parameterCache.computeIfAbsent(cacheKey, (k) -> { + + Map result = new HashMap<>(); + Map namedParameters = new HashMap<>(); + List positionalValues = new ArrayList<>(); + + Set possibleKeys = gatherAllPossibleKeys(methodParameter.getMethod()); + + // First, resolve all parameters passed by-name + for (int i = 0; i < words.size(); i++) { + String word = words.get(i); + if (possibleKeys.contains(word)) { + String key = word; + Parameter parameter = lookupParameterForKey(methodParameter.getMethod(), key, prefix); + int arity = getArity(parameter); + + if (i + 1 + arity > words.size()) { + String input = words.subList(i, words.size()).stream().collect(Collectors.joining(" ")); + throw new UnfinishedParameterResolutionException(describe(Utils.createMethodParameter(parameter)), input); + } + Assert.isTrue(i + 1 + arity <= words.size(), String.format("Not enough input for parameter '%s'", word)); + String raw = words.subList(i + 1, i + 1 + arity).stream().collect(Collectors.joining(",")); + Assert.isTrue(!namedParameters.containsKey(key), String.format("Parameter for '%s' has already been specified", word)); + namedParameters.put(key, raw); + result.put(parameter, ParameterRawValue.explicit(raw, key)); + i += arity; + if (arity == 0) { + boolean defaultValue = booleanDefaultValue(parameter); + // Boolean parameter has been specified. Use the opposite of the default value + result.put(parameter, ParameterRawValue.explicit(String.valueOf(!defaultValue), key)); + } + } // store for later processing of positional params + else { + positionalValues.add(word); + } + } + + // Now have a second pass over params and treat them as positional + int offset = 0; + Parameter[] parameters = methodParameter.getMethod().getParameters(); + for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) { + Parameter parameter = parameters[i]; + // Compute the intersection between possible keys for the param and what we've already seen for named params + Collection keys = getKeysForParameter(methodParameter.getMethod(), i).collect(Collectors.toSet()); + Collection copy = new HashSet<>(keys); + copy.retainAll(namedParameters.keySet()); + if (copy.isEmpty()) { // Was not set via a key (including aliases), must be positional + int arity = getArity(parameter); + if (arity > 0 && (offset + arity) <= positionalValues.size()) { + String raw = positionalValues.subList(offset, offset + arity).stream().collect(Collectors.joining(",")); + result.put(parameter, ParameterRawValue.explicit(raw, null)); + offset += arity; + } // No more input. Try defaultValues + else { + Optional defaultValue = defaultValueFor(parameter); + defaultValue.ifPresent(value -> result.put(parameter, ParameterRawValue.implicit(value, null))); + } + } + else if (copy.size() > 1) { + throw new IllegalArgumentException("Named parameter has been specified multiple times via " + quote(copy)); + } + } + + Assert.isTrue(offset == positionalValues.size(), "Too many arguments: the following could not be mapped to parameters: " + + positionalValues.subList(offset, positionalValues.size()).stream().collect(Collectors.joining(" ", "'", "'"))); + return result; + }); + + Parameter param = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()]; + if (!resolved.containsKey(param)) { + throw new ParameterMissingResolutionException(describe(methodParameter)); + } + String s = resolved.get(param).value; + if (ShellOption.NULL.equals(s)) { + return null; + } + else { + return conversionService.convert(s, TypeDescriptor.valueOf(String.class), new TypeDescriptor(methodParameter)); + } + } + + private Set gatherAllPossibleKeys(Method method) { + final String prefix = "--"; + return Arrays.stream(method.getParameters()) + .flatMap(p -> { + ShellOption option = p.getAnnotation(ShellOption.class); + if (option != null && option.value().length > 0) { + return Arrays.stream(option.value()); + } + else { + return Stream.of(prefix + Utils.createMethodParameter(p).getParameterName()); + } + }).collect(Collectors.toSet()); + } + + private String prefixForMethod(MethodParameter methodParameter) { + return methodParameter.getMethod().getAnnotation(ShellMethod.class).prefix(); + } + + private Optional defaultValueFor(Parameter parameter) { + Optional defaultValue = Optional.empty(); + ShellOption option = parameter.getAnnotation(ShellOption.class); + if (option != null && !ShellOption.NONE.equals(option.defaultValue())) { + defaultValue = Optional.of(option.defaultValue()); + } + else if (option == null && getArity(parameter) == 0) { + return Optional.of("false"); + } + return defaultValue; + } + + private boolean booleanDefaultValue(Parameter parameter) { + ShellOption option = parameter.getAnnotation(ShellOption.class); + if (option != null && !ShellOption.NULL.equals(option.defaultValue())) { + return Boolean.parseBoolean(option.defaultValue()); + } + return false; + } + + @Override + public ParameterDescription describe(MethodParameter parameter) { + Parameter jlrParameter = parameter.getMethod().getParameters()[parameter.getParameterIndex()]; + int arity = getArity(jlrParameter); + Class type = parameter.getParameterType(); + ShellOption option = jlrParameter.getAnnotation(ShellOption.class); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arity; i++) { + if (i > 0) { + sb.append(" "); + } + sb.append(arity > 1 ? unCamelify(removeMultiplicityFromType(parameter).getSimpleName()) : unCamelify(type.getSimpleName())); + } + ParameterDescription result = ParameterDescription.outOf(parameter); + result.formal(sb.toString()); + if (option != null) { + result.help(option.help()); + Optional defaultValue = defaultValueFor(jlrParameter); + if (defaultValue.isPresent()) { + result.defaultValue(defaultValue.map(dv -> dv.equals(ShellOption.NULL) ? "" : dv).get()); + } + } + result + .keys(getKeysForParameter(parameter.getMethod(), parameter.getParameterIndex()) + .collect(Collectors.toList())) + .mandatoryKey(false); + + return result; + } + + @Override + public List complete(MethodParameter methodParameter, CompletionContext context) { + boolean set; + Exception unfinished = null; + // First try to see if this parameter has been set, even to some unfinished value + ParameterRawValue parameterRawValue = null; + try { + resolve(methodParameter, context.getWords()); + CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), context.getWords()); + Parameter parameter = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()]; + parameterRawValue = parameterCache.get(cacheKey).get(parameter); + set = parameterRawValue.explicit; + } + catch (ParameterMissingResolutionException e) { + set = false; + } + catch (Exception e) { + unfinished = e; + set = false; + // Most likely what is already typed would fail resolution (eg type conversion failure) + // Exit early and let other parameters have a chance at being proposed + //return Collections.emptyList(); + } + + // There are 3 possible cases: + // 1) parameter not set at all + // 2) parameter set via its key, not enough input to consume a value + // 3) parameter set, and some value bound. But maybe that value is just a prefix to what the user actually wants + // 3.1) or maybe that value was resolved by position, but is a prefix of an actual valid key + + if (!set) { + if (unfinished == null) { // case 1 above + return commandsThatStartWithContextPrefix(methodParameter, context); + } // case 2 + else { + return valueCompletions(methodParameter, context); + } + } + else { + List result = new ArrayList<>(); + + String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : ""; + // TODO: should not look at last word only, but everything after what was used for key + // Case 3 + result.addAll(valueCompletions(methodParameter, context)); + + if (parameterRawValue.positional()) { + // Case 3.1: There exists "--command foo" and user has typed "--comm" which (wrongly) got resolved as a positional param + result.addAll(commandsThatStartWithContextPrefix(methodParameter, context)); + } + return result; + } + } + + private List valueCompletions(MethodParameter methodParameter, CompletionContext completionContext) { + return valueProviders.stream() + .filter(vp -> vp.supports(methodParameter, completionContext)) + .map(vp -> vp.complete(methodParameter, completionContext, null)) + .findFirst().orElseGet(() -> Collections.emptyList()); + } + + private List commandsThatStartWithContextPrefix(MethodParameter methodParameter, CompletionContext context) { + String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : ""; + return describe(methodParameter).keys().stream() + .filter(k -> k.startsWith(prefix)) + .map(v -> new CompletionProposal(v)) + .collect(Collectors.toList()); + } + + /** + * In case of {@code foo[] or Collection} and arity > 1, return the element type. + */ + private Class removeMultiplicityFromType(MethodParameter parameter) { + Class parameterType = parameter.getParameterType(); + if (parameterType.isArray()) { + return parameterType.getComponentType(); + } + else if (Collection.class.isAssignableFrom(parameterType)) { + return parameter.getNestedParameterType(); + } + else { + throw new RuntimeException("For " + parameter + " (with arity > 1) expected an array/collection type"); + } + } + + /** + * Surrounds the parameter keys with quotes. + */ + private String quote(Collection keys) { + return keys.stream().collect(Collectors.joining(", ", "'", "'")); + } + + /** + * Return the arity of a given parameter. The default arity is 1, except for + * booleans where arity is 0 (can be overridden back to 1 via an annotation) + */ + private int getArity(Parameter parameter) { + ShellOption option = parameter.getAnnotation(ShellOption.class); + int inferred = (parameter.getType() == boolean.class || parameter.getType() == Boolean.class) ? 0 : 1; + return option != null ? option.arity() : inferred; + } + + /** + * Return the key(s) the i-th parameter of the command method, resolved either from the {@link ShellOption} + * annotation, + * or from the actual parameter name. + */ + private Stream getKeysForParameter(Method method, int index) { + String prefix = "--"; + Parameter p = method.getParameters()[index]; + ShellOption option = p.getAnnotation(ShellOption.class); + if (option != null && option.value().length > 0) { + return Arrays.stream(option.value()); + } + else { + return Stream.of(prefix + Utils.createMethodParameter(p).getParameterName()); + } + } + + /** + * Return the method parameter that should be bound to the given key. + */ + private Parameter lookupParameterForKey(Method method, String key, String prefix) { + Parameter[] parameters = method.getParameters(); + for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) { + Parameter p = parameters[i]; + if (getKeysForParameter(method, i).anyMatch(k -> k.equals(key))) { + return p; + } + } + throw new IllegalArgumentException(String.format("Could not look up parameter for '%s%s' in %s", prefix, key, method)); + } + + private static class CacheKey { + + private final Method method; + + private final List words; + + private CacheKey(Method method, List words) { + this.method = method; + this.words = words; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheKey cacheKey = (CacheKey) o; + return Objects.equals(method, cacheKey.method) && + Objects.equals(words, cacheKey.words); + } + + @Override + public int hashCode() { + return Objects.hash(method, words); + } + } + + private static class ParameterRawValue { + + private CompletionContext context; + + private int from; + + private int to; + + private Integer keyIndex; + + /** + * The raw String value that got bound to a parameter. + */ + private final String value; + + /** + * If false, the value resolved is the result of applying defaults. + */ + private final boolean explicit; + + /** + * The key that was used to set the parameter, or null if resolution happened by position. + */ + private final String key; + + private ParameterRawValue(String value, boolean explicit, String key) { + this.value = value; + this.explicit = explicit; + this.key = key; + } + + public static ParameterRawValue explicit(String value, String key) { + return new ParameterRawValue(value, true, key); + } + + public static ParameterRawValue implicit(String value, String key) { + return new ParameterRawValue(value, false, key); + } + + public boolean positional() { + return key == null; + } + } + +} diff --git a/src/main/java/org/springframework/shell2/standard/ValueProvider.java b/src/main/java/org/springframework/shell2/standard/ValueProvider.java new file mode 100644 index 00000000..8892c7f9 --- /dev/null +++ b/src/main/java/org/springframework/shell2/standard/ValueProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.shell2.CompletionContext; +import org.springframework.shell2.CompletionProposal; + +/** + */ +public interface ValueProvider { + + boolean supports(MethodParameter parameter, CompletionContext completionContext); + + List complete(MethodParameter parameter, CompletionContext completionContext, String[] hints); +} diff --git a/src/main/java/org/springframework/shell2/standard/ValueProviderSupport.java b/src/main/java/org/springframework/shell2/standard/ValueProviderSupport.java new file mode 100644 index 00000000..8ef322d7 --- /dev/null +++ b/src/main/java/org/springframework/shell2/standard/ValueProviderSupport.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +import org.springframework.core.MethodParameter; +import org.springframework.shell2.CompletionContext; + +/** + */ +public abstract class ValueProviderSupport implements ValueProvider { + + @Override + public boolean supports(MethodParameter parameter, CompletionContext completionContext) { + ShellOption annotation = parameter.getParameterAnnotation(ShellOption.class); + if (annotation == null) { + return false; + } + return annotation.valueProvider().isAssignableFrom(this.getClass()); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 6ff6a9a4..9a8bf719 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,5 +1,5 @@ - - - - - + + + + + diff --git a/src/test/java/org/springframework/shell2/DefaultParameterResolverTest.java b/src/test/java/org/springframework/shell2/DefaultParameterResolverTest.java deleted file mode 100644 index 1a9d5acc..00000000 --- a/src/test/java/org/springframework/shell2/DefaultParameterResolverTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.MethodParameter; -import org.springframework.core.convert.support.DefaultConversionService; - -import java.lang.reflect.Method; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.util.ReflectionUtils.findMethod; - -/** - * Unit tests for DefaultParameterResolver. - * - * @author Eric Bottard - * @author Florent Biville - */ -public class DefaultParameterResolverTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private DefaultParameterResolver resolver = new DefaultParameterResolver(new DefaultConversionService()); - - @Test - public void testParses() throws Exception { - Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class); - - assertThat(resolver.resolve( - makeMethodParameter(method, 0), - asList("--force --name --foo y".split(" ")) - )).isEqualTo(true); - assertThat(resolver.resolve( - makeMethodParameter(method, 1), - asList("--force --name --foo y".split(" ")) - )).isEqualTo("--foo"); - assertThat(resolver.resolve( - makeMethodParameter(method, 2), - asList("--force --name --foo y".split(" ")) - )).isEqualTo("y"); - assertThat(resolver.resolve( - makeMethodParameter(method, 3), - asList("--force --name --foo y".split(" ")) - )).isEqualTo("last"); - - } - - @Test - public void testParameterSpecifiedTwiceViaDifferentAliases() throws Exception { - Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class); - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Named parameter has been specified multiple times via '--bar, --baz'"); - - resolver.resolve( - makeMethodParameter(method, 0), - asList("--force --name --foo y --bar x --baz z".split(" ")) - ); - } - - @Test - public void testParameterSpecifiedTwiceViaSameKey() throws Exception { - Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class); - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Parameter for '--baz' has already been specified"); - - resolver.resolve( - makeMethodParameter(method, 0), - asList("--force --name --foo y --baz x --baz z".split(" ")) - ); - } - - @Test - public void testUnknownParameter() throws Exception { - Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class); - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Could not look up parameter for '--unknown' in " + method); - - resolver.resolve( - makeMethodParameter(method, 0), - asList("--unknown --foo bar".split(" ")) - ); - } - - @Test - public void testTooMuchInput() throws Exception { - Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class); - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("the following could not be mapped to parameters: 'leftover'"); - - resolver.resolve( - makeMethodParameter(method, 0), - asList("--foo hello --name bar --force --bar well leftover".split(" ")) - ); - } - - private MethodParameter makeMethodParameter(Method method, int parameterIndex) { - MethodParameter methodParameter = new MethodParameter(method, parameterIndex); - methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - return methodParameter; - } - - -} diff --git a/src/test/java/org/springframework/shell2/UtilsTest.java b/src/test/java/org/springframework/shell2/UtilsTest.java index 3a7018c9..4d8ef95a 100644 --- a/src/test/java/org/springframework/shell2/UtilsTest.java +++ b/src/test/java/org/springframework/shell2/UtilsTest.java @@ -1,37 +1,37 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; - -/** - * Tests for {@link Utils}. - * - * @author Eric Bottard - */ -public class UtilsTest { - - @Test - public void testUnCamelify() throws Exception { - assertThat(Utils.unCamelify("HelloWorld")).isEqualTo("hello-world"); - assertThat(Utils.unCamelify("helloWorld")).isEqualTo("hello-world"); - assertThat(Utils.unCamelify("helloWorldHowAreYou")).isEqualTo("hello-world-how-are-you"); - assertThat(Utils.unCamelify("URL")).isEqualTo("url"); - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +/** + * Tests for {@link Utils}. + * + * @author Eric Bottard + */ +public class UtilsTest { + + @Test + public void testUnCamelify() throws Exception { + assertThat(Utils.unCamelify("HelloWorld")).isEqualTo("hello-world"); + assertThat(Utils.unCamelify("helloWorld")).isEqualTo("hello-world"); + assertThat(Utils.unCamelify("helloWorldHowAreYou")).isEqualTo("hello-world-how-are-you"); + assertThat(Utils.unCamelify("URL")).isEqualTo("url"); + } +} diff --git a/src/test/java/org/springframework/shell2/commands/HelpTest.java b/src/test/java/org/springframework/shell2/commands/HelpTest.java index 3e929ee8..61e9ea23 100644 --- a/src/test/java/org/springframework/shell2/commands/HelpTest.java +++ b/src/test/java/org/springframework/shell2/commands/HelpTest.java @@ -1,157 +1,157 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.commands; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.reflect.Method; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.assertj.core.api.Assertions; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; -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.convert.support.DefaultConversionService; -import org.springframework.core.io.ClassPathResource; -import org.springframework.shell2.DefaultParameterResolver; -import org.springframework.shell2.MethodTarget; -import org.springframework.shell2.ParameterResolver; -import org.springframework.shell2.Shell; -import org.springframework.shell2.ShellComponent; -import org.springframework.shell2.ShellMethod; -import org.springframework.shell2.ShellOption; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.ReflectionUtils; - -/** - * Tests for the {@link Help} command. - * - * @author Eric Bottard - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = HelpTest.Config.class) -public class HelpTest { - - @Autowired - private Help help; - - @Rule - public TestName testName = new TestName(); - - @Test - public void testCommandHelp() throws Exception { - CharSequence help = this.help.help("first-command").toString(); - Assertions.assertThat(help).isEqualTo(sample()); - } - - @Test - public void testCommandList() throws Exception { - String list = this.help.help(null).toString(); - Assertions.assertThat(list).isEqualTo(sample()); - } - - @Test(expected = IllegalArgumentException.class) - public void testUnknownCommand() throws Exception { - this.help.help("some unknown command"); - } - - private String sample() throws IOException { - InputStream is = new ClassPathResource(HelpTest.class.getSimpleName() + "-" + testName.getMethodName() + ".txt", HelpTest.class).getInputStream(); - return FileCopyUtils.copyToString(new InputStreamReader(is, "UTF-8")).replace("&", ""); - } - - @Configuration - static class Config { - - @Bean - public Help help() { - return new Help(Collections.singletonList(parameterResolver())); - } - - @Bean - public Shell shell() { - return () -> { - Map result = new HashMap<>(); - Method method = ReflectionUtils.findMethod(Commands.class, "firstCommand", boolean.class, boolean.class, int.class, float[].class); - MethodTarget methodTarget = new MethodTarget(method, commands(), "A rather extensive description of some command."); - result.put("first-command", methodTarget); - result.put("1st-command", methodTarget); - - method = ReflectionUtils.findMethod(Commands.class, "secondCommand"); - methodTarget = new MethodTarget(method, commands(), "The second command. This one is known under several aliases as well."); - result.put("second-command", methodTarget); - result.put("yet-another-command", methodTarget); - - method = ReflectionUtils.findMethod(Commands.class, "thirdCommand"); - methodTarget = new MethodTarget(method, commands(), "The last command."); - result.put("third-command", methodTarget); - - return result; - }; - } - - @Bean - public ParameterResolver parameterResolver() { - return new DefaultParameterResolver(new DefaultConversionService()); - } - - @Bean - public Object commands() { - return new Commands(); - } - - } - - @ShellComponent - static class Commands { - - @ShellMethod(prefix = "-") - public void firstCommand( - // Single key and arity = 0. Help displayed on same line - @ShellOption(help = "Whether to delete recursively", arity = 0) boolean r, - // Multiple keys and arity 0. Help displayed on next line - @ShellOption(help = "Do not ask for confirmation. YOLO", arity = 0, value = {"f", "-force"}) boolean force, - // Single key, arity >= 1. Help displayed on next line. Optional - @ShellOption(help = "The answer to everything", defaultValue = "42") int n, - // Single key, arity > 1. - @ShellOption(help = "Some other parameters", arity = 3) float[] o - ) { - - } - - @ShellMethod - public void secondCommand() { - - } - - @ShellMethod - public void thirdCommand() { - - } - - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.commands; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +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.convert.support.DefaultConversionService; +import org.springframework.core.io.ClassPathResource; +import org.springframework.shell2.standard.StandardParameterResolver; +import org.springframework.shell2.MethodTarget; +import org.springframework.shell2.ParameterResolver; +import org.springframework.shell2.Shell; +import org.springframework.shell2.standard.ShellComponent; +import org.springframework.shell2.standard.ShellMethod; +import org.springframework.shell2.standard.ShellOption; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Tests for the {@link Help} command. + * + * @author Eric Bottard + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = HelpTest.Config.class) +public class HelpTest { + + @Autowired + private Help help; + + @Rule + public TestName testName = new TestName(); + + @Test + public void testCommandHelp() throws Exception { + CharSequence help = this.help.help("first-command").toString(); + Assertions.assertThat(help).isEqualTo(sample()); + } + + @Test + public void testCommandList() throws Exception { + String list = this.help.help(null).toString(); + Assertions.assertThat(list).isEqualTo(sample()); + } + + @Test(expected = IllegalArgumentException.class) + public void testUnknownCommand() throws Exception { + this.help.help("some unknown command"); + } + + private String sample() throws IOException { + InputStream is = new ClassPathResource(HelpTest.class.getSimpleName() + "-" + testName.getMethodName() + ".txt", HelpTest.class).getInputStream(); + return FileCopyUtils.copyToString(new InputStreamReader(is, "UTF-8")).replace("&", ""); + } + + @Configuration + static class Config { + + @Bean + public Help help() { + return new Help(Collections.singletonList(parameterResolver())); + } + + @Bean + public Shell shell() { + return () -> { + Map result = new HashMap<>(); + Method method = ReflectionUtils.findMethod(Commands.class, "firstCommand", boolean.class, boolean.class, int.class, float[].class); + MethodTarget methodTarget = new MethodTarget(method, commands(), "A rather extensive description of some command."); + result.put("first-command", methodTarget); + result.put("1st-command", methodTarget); + + method = ReflectionUtils.findMethod(Commands.class, "secondCommand"); + methodTarget = new MethodTarget(method, commands(), "The second command. This one is known under several aliases as well."); + result.put("second-command", methodTarget); + result.put("yet-another-command", methodTarget); + + method = ReflectionUtils.findMethod(Commands.class, "thirdCommand"); + methodTarget = new MethodTarget(method, commands(), "The last command."); + result.put("third-command", methodTarget); + + return result; + }; + } + + @Bean + public ParameterResolver parameterResolver() { + return new StandardParameterResolver(new DefaultConversionService()); + } + + @Bean + public Object commands() { + return new Commands(); + } + + } + + @ShellComponent + static class Commands { + + @ShellMethod(prefix = "--") + public void firstCommand( + // Single key and arity = 0. Help displayed on same line + @ShellOption(help = "Whether to delete recursively", arity = 0, value = "-r") boolean r, + // Multiple keys and arity 0. Help displayed on next line + @ShellOption(help = "Do not ask for confirmation. YOLO", arity = 0, value = {"-f", "--force"}) boolean force, + // Single key, arity >= 1. Help displayed on next line. Optional + @ShellOption(help = "The answer to everything", defaultValue = "42", value = "-n") int n, + // Single key, arity > 1. + @ShellOption(help = "Some other parameters", arity = 3, value = "-o") float[] o + ) { + + } + + @ShellMethod + public void secondCommand() { + + } + + @ShellMethod + public void thirdCommand() { + + } + + } +} diff --git a/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java b/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java index a5243dd1..9a50492f 100644 --- a/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java +++ b/src/test/java/org/springframework/shell2/jcommander/FieldCollins.java @@ -1,61 +1,61 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.jcommander; - -import java.util.ArrayList; -import java.util.List; - -import com.beust.jcommander.Parameter; - -/** - * Created by ericbottard on 15/12/15. - */ -public class FieldCollins { - - @Parameter(names = "--name") - private String name; - - @Parameter(names = "-level") - private int level; - - @Parameter(description = "rest") - private List rest = new ArrayList<>(); - - public List getRest() { - return rest; - } - - public void setRest(List rest) { - this.rest = rest; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getLevel() { - return level; - } - - public void setLevel(int level) { - this.level = level; - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.jcommander; + +import java.util.ArrayList; +import java.util.List; + +import com.beust.jcommander.Parameter; + +/** + * Created by ericbottard on 15/12/15. + */ +public class FieldCollins { + + @Parameter(names = "--name") + private String name; + + @Parameter(names = "-level") + private int level; + + @Parameter(description = "rest") + private List rest = new ArrayList<>(); + + public List getRest() { + return rest; + } + + public void setRest(List rest) { + this.rest = rest; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getLevel() { + return level; + } + + public void setLevel(int level) { + this.level = level; + } +} diff --git a/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java b/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java index e89d1411..37be2556 100644 --- a/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java +++ b/src/test/java/org/springframework/shell2/jcommander/JCommanderParameterResolverTest.java @@ -1,59 +1,61 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.jcommander; - -import org.junit.Test; -import org.springframework.core.MethodParameter; -import org.springframework.util.ReflectionUtils; - -import java.lang.reflect.Method; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Created by ericbottard on 15/12/15. - */ -public class JCommanderParameterResolverTest { - - private static final Method COMMAND_METHOD = ReflectionUtils.findMethod(MyLordCommands.class, "genesis", FieldCollins.class); - - private JCommanderParameterResolver resolver = new JCommanderParameterResolver(); - - @Test - public void testSupportsJCommanderPojos() throws Exception { - assertThat(resolver.supports(new MethodParameter(COMMAND_METHOD, 0))).isEqualTo(true); - } - - @Test - public void testDoesNotSupportsNonJCommanderPojos() throws Exception { - Method method = ReflectionUtils.findMethod(MyLordCommands.class, "apocalypse", String.class); - - assertThat(resolver.supports(new MethodParameter(method, 0))).isFalse(); - } - - @Test - public void testPojoValuesAreCorrectlySet() { - MethodParameter methodParameter = new MethodParameter(COMMAND_METHOD, 0); - - FieldCollins resolved = (FieldCollins) resolver.resolve(methodParameter, asList("--name foo -level 2 something-else yet-something-else".split(" "))); - - assertThat(resolved.getName()).isEqualTo("foo"); - assertThat(resolved.getLevel()).isEqualTo(2); - assertThat(resolved.getRest()).containsOnlyOnce("something-else", "yet-something-else"); - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.jcommander; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.shell2.Utils; +import org.springframework.util.ReflectionUtils; + +/** + * Created by ericbottard on 15/12/15. + */ +public class JCommanderParameterResolverTest { + + private static final Method COMMAND_METHOD = ReflectionUtils.findMethod(MyLordCommands.class, "genesis", FieldCollins.class); + + private JCommanderParameterResolver resolver = new JCommanderParameterResolver(); + + @Test + public void testSupportsJCommanderPojos() throws Exception { + assertThat(resolver.supports(Utils.createMethodParameter(COMMAND_METHOD, 0))).isEqualTo(true); + } + + @Test + public void testDoesNotSupportsNonJCommanderPojos() throws Exception { + Method method = ReflectionUtils.findMethod(MyLordCommands.class, "apocalypse", String.class); + + assertThat(resolver.supports(Utils.createMethodParameter(method, 0))).isFalse(); + } + + @Test + public void testPojoValuesAreCorrectlySet() { + MethodParameter methodParameter = Utils.createMethodParameter(COMMAND_METHOD, 0); + + FieldCollins resolved = (FieldCollins) resolver.resolve(methodParameter, asList("--name foo -level 2 something-else yet-something-else".split(" "))); + + assertThat(resolved.getName()).isEqualTo("foo"); + assertThat(resolved.getLevel()).isEqualTo(2); + assertThat(resolved.getRest()).containsOnlyOnce("something-else", "yet-something-else"); + } +} diff --git a/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java b/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java index d47bc86b..236af2d5 100644 --- a/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java +++ b/src/test/java/org/springframework/shell2/jcommander/MyLordCommands.java @@ -1,32 +1,32 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.jcommander; - -/** - * Created by ericbottard on 15/12/15. - */ -public class MyLordCommands { - - public void genesis(FieldCollins fieldCollins) { - - } - - public void apocalypse(String param) { - - } - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.jcommander; + +/** + * Created by ericbottard on 15/12/15. + */ +public class MyLordCommands { + + public void genesis(FieldCollins fieldCollins) { + + } + + public void apocalypse(String param) { + + } + +} diff --git a/src/test/java/org/springframework/shell2/legacy/ArtifactType.java b/src/test/java/org/springframework/shell2/legacy/ArtifactType.java index e8c5849f..10891465 100644 --- a/src/test/java/org/springframework/shell2/legacy/ArtifactType.java +++ b/src/test/java/org/springframework/shell2/legacy/ArtifactType.java @@ -1,25 +1,25 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.legacy; - -/** - * Created by ericbottard on 09/12/15. - */ -public enum ArtifactType { - - source, processor, sink, task -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 index 39b3e5d9..aeb4f7c1 100644 --- a/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java +++ b/src/test/java/org/springframework/shell2/legacy/LegacyCommands.java @@ -1,61 +1,61 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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(value = "sum", help = "adds two numbers") - public int sum(@CliOption(key = "v1", unspecifiedDefaultValue = "38") int a, @CliOption(key = "v2", specifiedDefaultValue = "42") int b) { - return a + b; - } - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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(value = "sum", help = "adds two numbers") + 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 index 907e0c25..42dec593 100644 --- a/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java +++ b/src/test/java/org/springframework/shell2/legacy/LegacyMethodTargetResolverTest.java @@ -1,76 +1,76 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.legacy; - - -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; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.data.MapEntry.entry; - -/** - * 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).contains(entry( - "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(); - } - } - -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.legacy; + + +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; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +/** + * 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).contains(entry( + "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 index 210a091d..95f912b5 100644 --- a/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java +++ b/src/test/java/org/springframework/shell2/legacy/LegacyParameterResolverTest.java @@ -1,191 +1,184 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell2.legacy; - -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.DefaultParameterNameDiscoverer; -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; - -import java.lang.reflect.Method; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.shell2.legacy.LegacyCommands.REGISTER_METHOD; - -@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 = buildMethodParameter(REGISTER_METHOD, NAME_OR_ANONYMOUS); - - boolean result = parameterResolver.supports(methodParameter); - - assertThat(result).isTrue(); - } - - @Test - public void resolvesParameterAnnotatedWithCliOption() throws Exception { - MethodParameter methodParameter = buildMethodParameter(REGISTER_METHOD, NAME_OR_ANONYMOUS); - - Object result = resolve(methodParameter, "--foo bar --name baz --qix bux"); - - assertThat(result).isEqualTo("baz"); - } - - @Test - public void resolvesAnonymousParameterAnnotatedWithCliOption() throws Exception { - MethodParameter methodParameter = buildMethodParameter(REGISTER_METHOD, NAME_OR_ANONYMOUS); - - Object result = resolve(methodParameter, "--foo bar baz --qix bux"); - assertThat(result).isEqualTo("baz"); - - // As first param - result = resolve(methodParameter, "baz --foo bar --qix bux"); - assertThat(result).isEqualTo("baz"); - } - - @Test - public void usesLegacyConverters() throws Exception { - MethodParameter methodParameter = buildMethodParameter(REGISTER_METHOD, TYPE); - - Object result = resolve(methodParameter, "--foo bar --name baz --qix bux --type processor"); - - assertThat(result).isSameAs(ArtifactType.processor); - } - - @Test - public void testUnspecifiedDefaultValue() throws Exception { - MethodParameter methodParameter = buildMethodParameter(REGISTER_METHOD, FORCE); - - Object result = resolve(methodParameter, "--foo bar --name baz --qix bux"); - - assertThat(result).isEqualTo(false); - } - - @Test - public void testSpecifiedDefaultValue() throws Exception { - MethodParameter methodParameter = buildMethodParameter(REGISTER_METHOD, FORCE); - - assertThat(resolve(methodParameter, "--force --foo bar --name baz --qix bux")).isEqualTo(true); - assertThat(resolve(methodParameter, "--foo bar --name baz --qix bux --force")).isEqualTo(true); - } - - @Test - public void testParameterNotFound() throws Exception { - MethodParameter methodParameter = buildMethodParameter(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]"); - resolve(methodParameter, "--force --foo bar --name baz --qix bux"); - } - - @Test - public void testParameterFoundWithSameNameTooManyTimes() throws Exception { - MethodParameter methodParameter = buildMethodParameter(REGISTER_METHOD, COORDINATES); - - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Option --coordinates has already been set"); - resolve(methodParameter, "--force --coordinates bar --coordinates baz --qix bux"); - } - - @Test - public void testNoConverterFound() throws Exception { - MethodParameter methodParameter = buildMethodParameter(LegacyCommands.SUM_METHOD, 0); - - thrown.expect(IllegalStateException.class); - thrown.expectMessage("No converter found for --v1 from '1' to type int"); - resolve(methodParameter, "--v1 1 --v2 2"); - } - - @Test - public void testNoConverterFoundForUnspecifiedValue() throws Exception { - MethodParameter methodParameter = buildMethodParameter(LegacyCommands.SUM_METHOD, 0); - - thrown.expect(IllegalStateException.class); - thrown.expectMessage("No converter found for --v1 from '38' to type int"); - resolve(methodParameter, "--v2 2"); - } - - @Test - public void testNoConverterFoundForSpecifiedValue() throws Exception { - MethodParameter methodParameter = buildMethodParameter(LegacyCommands.SUM_METHOD, 1); - - thrown.expect(IllegalStateException.class); - thrown.expectMessage("No converter found for --v2 from '42' to type int"); - resolve(methodParameter, "--v1 1 --v2"); - } - - private MethodParameter buildMethodParameter(Method method, int index) { - MethodParameter methodParameter = new MethodParameter(method, index); - methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - return methodParameter; - } - - private Object resolve(MethodParameter methodParameter, String command) { - return parameterResolver.resolve(methodParameter, asList(command.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(); - } - } -} +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.legacy; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.shell2.legacy.LegacyCommands.REGISTER_METHOD; + +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.shell2.Utils; +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 = Utils.createMethodParameter(REGISTER_METHOD, NAME_OR_ANONYMOUS); + + boolean result = parameterResolver.supports(methodParameter); + + assertThat(result).isTrue(); + } + + @Test + public void resolvesParameterAnnotatedWithCliOption() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, NAME_OR_ANONYMOUS); + + Object result = resolve(methodParameter, "--foo bar --name baz --qix bux"); + + assertThat(result).isEqualTo("baz"); + } + + @Test + public void resolvesAnonymousParameterAnnotatedWithCliOption() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, NAME_OR_ANONYMOUS); + + Object result = resolve(methodParameter, "--foo bar baz --qix bux"); + assertThat(result).isEqualTo("baz"); + + // As first param + result = resolve(methodParameter, "baz --foo bar --qix bux"); + assertThat(result).isEqualTo("baz"); + } + + @Test + public void usesLegacyConverters() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, TYPE); + + Object result = resolve(methodParameter, "--foo bar --name baz --qix bux --type processor"); + + assertThat(result).isSameAs(ArtifactType.processor); + } + + @Test + public void testUnspecifiedDefaultValue() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, FORCE); + + Object result = resolve(methodParameter, "--foo bar --name baz --qix bux"); + + assertThat(result).isEqualTo(false); + } + + @Test + public void testSpecifiedDefaultValue() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, FORCE); + + assertThat(resolve(methodParameter, "--force --foo bar --name baz --qix bux")).isEqualTo(true); + assertThat(resolve(methodParameter, "--foo bar --name baz --qix bux --force")).isEqualTo(true); + } + + @Test + public void testParameterNotFound() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(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]"); + resolve(methodParameter, "--force --foo bar --name baz --qix bux"); + } + + @Test + public void testParameterFoundWithSameNameTooManyTimes() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(REGISTER_METHOD, COORDINATES); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Option --coordinates has already been set"); + resolve(methodParameter, "--force --coordinates bar --coordinates baz --qix bux"); + } + + @Test + public void testNoConverterFound() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SUM_METHOD, 0); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("No converter found for --v1 from '1' to type int"); + resolve(methodParameter, "--v1 1 --v2 2"); + } + + @Test + public void testNoConverterFoundForUnspecifiedValue() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SUM_METHOD, 0); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("No converter found for --v1 from '38' to type int"); + resolve(methodParameter, "--v2 2"); + } + + @Test + public void testNoConverterFoundForSpecifiedValue() throws Exception { + MethodParameter methodParameter = Utils.createMethodParameter(LegacyCommands.SUM_METHOD, 1); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("No converter found for --v2 from '42' to type int"); + resolve(methodParameter, "--v1 1 --v2"); + } + + private Object resolve(MethodParameter methodParameter, String command) { + return parameterResolver.resolve(methodParameter, asList(command.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(); + } + } +} diff --git a/src/test/java/org/springframework/shell2/standard/Remote.java b/src/test/java/org/springframework/shell2/standard/Remote.java new file mode 100644 index 00000000..0f7b3025 --- /dev/null +++ b/src/test/java/org/springframework/shell2/standard/Remote.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.core.MethodParameter; +import org.springframework.shell2.CompletionContext; +import org.springframework.shell2.CompletionProposal; + +/** + * An example commands class. + * + * @author Eric Bottard + * @author Florent Biville + */ +public class Remote { + + /** + * A command method that showcases
    + *
  • default handling for booleans (force)
  • + *
  • default parameter name discovery (name)
  • + *
  • default value supplying (foo and bar)
  • + *
+ */ + @ShellMethod(help = "switch channels") + public void zap(boolean force, + String name, + @ShellOption(defaultValue="defoolt") String foo, + @ShellOption(value = {"--bar", "--baz"}, defaultValue = "last") String bar) { + + } + + @ShellMethod(help = "bye bye") + public void shutdown(@ShellOption Delay delay) { + + } + + @ShellMethod(help = "add 3 numbers together") + public void add(@ShellOption(arity = 3, valueProvider = NumberValueProvider.class) List numbers) { + + } + + public enum Delay { + small, medium, big; + } + + + public static class NumberValueProvider extends ValueProviderSupport { + + @Override + public List complete(MethodParameter parameter, CompletionContext completionContext, String[] hints) { + String prefix = completionContext.currentWord() != null ? completionContext.currentWord() : ""; + return Arrays.asList("12", "42", "7").stream() + .filter(n -> n.startsWith(prefix)) + .map(n -> new CompletionProposal(n)) + .collect(Collectors.toList()); + } + } +} diff --git a/src/test/java/org/springframework/shell2/standard/StandardParameterResolverTest.java b/src/test/java/org/springframework/shell2/standard/StandardParameterResolverTest.java new file mode 100644 index 00000000..8abbb811 --- /dev/null +++ b/src/test/java/org/springframework/shell2/standard/StandardParameterResolverTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell2.standard; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.util.ReflectionUtils.findMethod; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.jline.reader.ParsedLine; +import org.jline.reader.impl.DefaultParser; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.shell2.CompletionContext; +import org.springframework.shell2.CompletionProposal; +import org.springframework.shell2.ParameterMissingResolutionException; +import org.springframework.shell2.UnfinishedParameterResolutionException; +import org.springframework.shell2.Utils; + +/** + * Unit tests for DefaultParameterResolver. + * @author Eric Bottard + * @author Florent Biville + */ +public class StandardParameterResolverTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService()); + + // Tests for resolution + + @Test + public void testParses() throws Exception { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "zap", boolean.class, String.class, String.class, String.class); + + assertThat(resolver.resolve( + Utils.createMethodParameter(method, 0), + asList("--force --name --foo y".split(" ")) + )).isEqualTo(true); + assertThat(resolver.resolve( + Utils.createMethodParameter(method, 1), + asList("--force --name --foo y".split(" ")) + )).isEqualTo("--foo"); + assertThat(resolver.resolve( + Utils.createMethodParameter(method, 2), + asList("--force --name --foo y".split(" ")) + )).isEqualTo("y"); + assertThat(resolver.resolve( + Utils.createMethodParameter(method, 3), + asList("--force --name --foo y".split(" ")) + )).isEqualTo("last"); + + } + + @Test + public void testParameterSpecifiedTwiceViaDifferentAliases() throws Exception { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "zap", boolean.class, String.class, String.class, String.class); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Named parameter has been specified multiple times via '--bar, --baz'"); + + resolver.resolve( + Utils.createMethodParameter(method, 0), + asList("--force --name --foo y --bar x --baz z".split(" ")) + ); + } + + @Test + public void testParameterSpecifiedTwiceViaSameKey() throws Exception { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "zap", boolean.class, String.class, String.class, String.class); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Parameter for '--baz' has already been specified"); + + resolver.resolve( + Utils.createMethodParameter(method, 0), + asList("--force --name --foo y --baz x --baz z".split(" ")) + ); + } + + @Test + public void testTooMuchInput() throws Exception { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "zap", boolean.class, String.class, String.class, String.class); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("the following could not be mapped to parameters: 'leftover'"); + + resolver.resolve( + Utils.createMethodParameter(method, 0), + asList("--foo hello --name bar --force --bar well leftover".split(" ")) + ); + } + + @Test + public void testIncompleteCommandResolution() throws Exception { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "shutdown", org.springframework.shell2.standard.Remote.Delay.class); + + thrown.expect(UnfinishedParameterResolutionException.class); + thrown.expectMessage("Error trying to resolve '--delay delay' using [--delay]"); + + resolver.resolve( + Utils.createMethodParameter(method, 0), + asList("--delay".split(" ")) + ); + } + + @Test + public void testIncompleteCommandResolutionBigArity() throws Exception { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "add", List.class); + + thrown.expect(UnfinishedParameterResolutionException.class); + thrown.expectMessage("Error trying to resolve '--numbers list list list' using [--numbers 1 2]"); + + resolver.resolve( + Utils.createMethodParameter(method, 0), + asList("--numbers 1 2".split(" ")) + ); + } + + @Test + public void testUnresolvableArg() throws Exception { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "zap", boolean.class, String.class, String.class, String.class); + + thrown.expect(ParameterMissingResolutionException.class); + thrown.expectMessage("Parameter '--name string' should be specified"); + + resolver.resolve( + Utils.createMethodParameter(method, 1), + asList("--foo hello --force --bar well".split(" ")) + ); + } + + // Tests for completion + + @Test + public void testParameterKeyNotYetSetAppearsInProposals() { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "zap", boolean.class, String.class, String.class, String.class); + List completions = resolver.complete( + Utils.createMethodParameter(method, 1), + contextFor("") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).contains("--name"); + completions = resolver.complete( + Utils.createMethodParameter(method, 1), + contextFor("--force ") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).contains("--name"); + } + + @Test + public void testParameterKeyNotFullySpecified() { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "zap", boolean.class, String.class, String.class, String.class); + List completions = resolver.complete( + Utils.createMethodParameter(method, 1), + contextFor("--na") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).contains("--name"); + completions = resolver.complete( + Utils.createMethodParameter(method, 1), + contextFor("--force --na") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).contains("--name"); + } + + @Test + public void testNoMoreAvailableParameters() { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "zap", boolean.class, String.class, String.class, String.class); + List completions = resolver.complete( + Utils.createMethodParameter(method, 2), // trying to complete --foo + contextFor("--name ") // but input is currently focused on --name + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + System.out.println(completions); +// assertThat(completions).isEmpty(); + } + + @Test + public void testNotTheRightTimeToCompleteThatParameter() { + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "shutdown", org.springframework.shell2.standard.Remote.Delay.class); + List completions = resolver.complete( + Utils.createMethodParameter(method, 0), + contextFor("--delay 323") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).isEmpty(); + } + + @Test + public void testValueCompletionWithNonDefaultArity() { + + resolver.setValueProviders(Arrays.asList(new Remote.NumberValueProvider())); + + Method method = findMethod(org.springframework.shell2.standard.Remote.class, "add", List.class); + List completions = resolver.complete( + Utils.createMethodParameter(method, 0), + contextFor("--numbers ") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).contains("12"); + + completions = resolver.complete( + Utils.createMethodParameter(method, 0), + contextFor("--numbers 42 ") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).contains("12"); + + completions = resolver.complete( + Utils.createMethodParameter(method, 0), + contextFor("--numbers 42 34 ") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).contains("12"); + + completions = resolver.complete( + Utils.createMethodParameter(method, 0), + contextFor("--numbers 42 34 66 ") + ).stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(completions).isEmpty(); + } + + + + private CompletionContext contextFor(String input) { + DefaultParser defaultParser = new DefaultParser(); + ParsedLine parsed = defaultParser.parse(input, input.length()); + List words = parsed.words().stream().filter(w -> w.length() > 0).collect(Collectors.toList()); + + return new CompletionContext(words, parsed.wordIndex(), parsed.wordCursor()); + } +}