Rework command subsystem
- Focus of these changes are to introduce a new command system based on real registrations (new way) instead of continuously (old way) resolve methods and its parameters via reflection. - There's a lot of changes as this resolution via reflection had its hooks almost everywhere and thus most changes are just refactorings. - Order to understand real changes I'd start to look classes under `org.springframework.shell.command` package as it defines new registration, catalog and parser classes. Also samples contain new classes to demonstrate new functionality. - Fixes #380
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 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.
|
||||
@@ -26,9 +26,9 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.Shell;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
import org.springframework.shell.style.TemplateExecutor;
|
||||
import org.springframework.shell.style.ThemeResolver;
|
||||
|
||||
@@ -47,9 +47,9 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
|
||||
private ObjectProvider<Terminal> terminalProvider;
|
||||
|
||||
private ObjectProvider<CommandRegistry> commandRegistryProvider;
|
||||
private ObjectProvider<CommandCatalog> commandCatalogProvider;
|
||||
|
||||
private ObjectProvider<ParameterResolver> parameterResolverProvider;
|
||||
private ObjectProvider<CompletionResolver> completionResolverProvider;
|
||||
|
||||
private ObjectProvider<TemplateExecutor> templateExecutorProvider;
|
||||
|
||||
@@ -69,8 +69,8 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
shellProvider = applicationContext.getBeanProvider(Shell.class);
|
||||
terminalProvider = applicationContext.getBeanProvider(Terminal.class);
|
||||
commandRegistryProvider = applicationContext.getBeanProvider(CommandRegistry.class);
|
||||
parameterResolverProvider = applicationContext.getBeanProvider(ParameterResolver.class);
|
||||
commandCatalogProvider = applicationContext.getBeanProvider(CommandCatalog.class);
|
||||
completionResolverProvider = applicationContext.getBeanProvider(CompletionResolver.class);
|
||||
templateExecutorProvider = applicationContext.getBeanProvider(TemplateExecutor.class);
|
||||
themeResolverProvider = applicationContext.getBeanProvider(ThemeResolver.class);
|
||||
}
|
||||
@@ -91,12 +91,12 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
return terminalProvider.getObject();
|
||||
}
|
||||
|
||||
protected CommandRegistry getCommandRegistry() {
|
||||
return commandRegistryProvider.getObject();
|
||||
protected CommandCatalog getCommandCatalog() {
|
||||
return commandCatalogProvider.getObject();
|
||||
}
|
||||
|
||||
protected Stream<ParameterResolver> getParameterResolver() {
|
||||
return parameterResolverProvider.orderedStream();
|
||||
protected Stream<CompletionResolver> getCompletionResolver() {
|
||||
return completionResolverProvider.orderedStream();
|
||||
}
|
||||
|
||||
protected TemplateExecutor getTemplateExecutor() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -20,9 +20,9 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
|
||||
/**
|
||||
* A {@link ValueProvider} that can be used to auto-complete names of shell commands.
|
||||
@@ -31,15 +31,15 @@ import org.springframework.shell.CompletionProposal;
|
||||
*/
|
||||
public class CommandValueProvider extends ValueProviderSupport {
|
||||
|
||||
private final CommandRegistry commandRegistry;
|
||||
private final CommandCatalog commandRegistry;
|
||||
|
||||
public CommandValueProvider(CommandRegistry commandRegistry) {
|
||||
public CommandValueProvider(CommandCatalog commandRegistry) {
|
||||
this.commandRegistry = commandRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompletionProposal> complete(MethodParameter parameter, CompletionContext completionContext, String[] hints) {
|
||||
return commandRegistry.listCommands().keySet().stream()
|
||||
return commandRegistry.getRegistrations().keySet().stream()
|
||||
.map(CompletionProposal::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.shell.standard;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandlingException;
|
||||
import org.springframework.shell.support.AbstractArgumentMethodArgumentResolver;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Resolver for {@link ShellOption @ShellOption} arguments.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ShellOptionMethodArgumentResolver extends AbstractArgumentMethodArgumentResolver {
|
||||
|
||||
public ShellOptionMethodArgumentResolver(ConversionService conversionService,
|
||||
@Nullable ConfigurableBeanFactory beanFactory) {
|
||||
super(conversionService, beanFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(ShellOption.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
|
||||
ShellOption annot = parameter.getParameterAnnotation(ShellOption.class);
|
||||
Assert.state(annot != null, "No ShellOption annotation");
|
||||
List<String> names = Arrays.stream(annot.value()).map(v -> StringUtils.trimLeadingCharacter(v, '-')).collect(Collectors.toList());
|
||||
return new HeaderNamedValueInfo(annot, names);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, List<String> names)
|
||||
throws Exception {
|
||||
for (String name : names) {
|
||||
if (message.getHeaders().containsKey(ARGUMENT_PREFIX + name)) {
|
||||
return message.getHeaders().get(ARGUMENT_PREFIX + name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMissingValue(List<String> headerName, MethodParameter parameter, Message<?> message) {
|
||||
throw new MessageHandlingException(message,
|
||||
"Missing headers '" + StringUtils.collectionToCommaDelimitedString(headerName)
|
||||
+ "' for method parameter type [" + parameter.getParameterType() + "]");
|
||||
}
|
||||
|
||||
private static final class HeaderNamedValueInfo extends NamedValueInfo {
|
||||
|
||||
private HeaderNamedValueInfo(ShellOption annotation, List<String> names) {
|
||||
super(names, false, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,21 +26,27 @@ import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.Command;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.MethodTargetRegistrar;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.CommandRegistration.Builder;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionSpec;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.springframework.util.StringUtils.collectionToDelimitedString;
|
||||
|
||||
/**
|
||||
* The standard implementation of {@link MethodTargetRegistrar} for new shell
|
||||
* applications, resolves methods annotated with {@link ShellMethod} on
|
||||
@@ -49,20 +55,20 @@ import static org.springframework.util.StringUtils.collectionToDelimitedString;
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
* @author Camilo Gonzalez
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, ApplicationContextAware {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(StandardMethodTargetRegistrar.class);
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private Map<String, MethodTarget> commands = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ConfigurableCommandRegistry registry) {
|
||||
public void register(CommandCatalog registry) {
|
||||
Map<String, Object> commandBeans = applicationContext.getBeansWithAnnotation(ShellComponent.class);
|
||||
for (Object bean : commandBeans.values()) {
|
||||
Class<?> clazz = bean.getClass();
|
||||
@@ -74,11 +80,78 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App
|
||||
}
|
||||
String group = getOrInferGroup(method);
|
||||
for (String key : keys) {
|
||||
log.debug("Registering with keys='{}' key='{}'", keys, key);
|
||||
Supplier<Availability> availabilityIndicator = findAvailabilityIndicator(keys, bean, method);
|
||||
MethodTarget target = new MethodTarget(method, bean, new Command.Help(shellMapping.value(), group),
|
||||
availabilityIndicator, shellMapping.interactionMode());
|
||||
registry.register(key, target);
|
||||
commands.put(key, target);
|
||||
|
||||
Builder builder = CommandRegistration.builder()
|
||||
.command(key)
|
||||
.group(group)
|
||||
.help(shellMapping.value())
|
||||
.interactionMode(shellMapping.interactionMode())
|
||||
.availability(availabilityIndicator);
|
||||
|
||||
InvocableHandlerMethod ihm = new InvocableHandlerMethod(bean, method);
|
||||
for (MethodParameter mp : ihm.getMethodParameters()) {
|
||||
|
||||
ShellOption so = mp.getParameterAnnotation(ShellOption.class);
|
||||
log.debug("Registering with mp='{}' so='{}'", mp, so);
|
||||
if (so != null) {
|
||||
List<String> longNames = new ArrayList<>();
|
||||
List<Character> shortNames = new ArrayList<>();
|
||||
if (!ObjectUtils.isEmpty(so.value())) {
|
||||
Arrays.asList(so.value()).stream().forEach(o -> {
|
||||
String stripped = StringUtils.trimLeadingCharacter(o, '-');
|
||||
log.debug("Registering o='{}' stripped='{}'", o, stripped);
|
||||
if (o.length() == stripped.length() + 2) {
|
||||
longNames.add(stripped);
|
||||
}
|
||||
else if (o.length() == stripped.length() + 1 && stripped.length() == 1) {
|
||||
shortNames.add(stripped.charAt(0));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
// ShellOption value not defined
|
||||
mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
|
||||
String longName = mp.getParameterName();
|
||||
Class<?> parameterType = mp.getParameterType();
|
||||
if (longName != null) {
|
||||
log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType);
|
||||
longNames.add(longName);
|
||||
}
|
||||
}
|
||||
if (!longNames.isEmpty() || !shortNames.isEmpty()) {
|
||||
log.debug("Registering longNames='{}' shortNames='{}'", longNames, shortNames);
|
||||
OptionSpec optionSpec = builder.withOption()
|
||||
.type(mp.getParameterType())
|
||||
.longNames(longNames.toArray(new String[0]))
|
||||
.shortNames(shortNames.toArray(new Character[0]))
|
||||
.position(mp.getParameterIndex())
|
||||
.description(so.help());
|
||||
if (so.arity() > -1) {
|
||||
optionSpec.arity(0, so.arity());
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
|
||||
String longName = mp.getParameterName();
|
||||
Class<?> parameterType = mp.getParameterType();
|
||||
if (longName != null) {
|
||||
log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType);
|
||||
builder.withOption()
|
||||
.longNames(longName)
|
||||
.type(parameterType)
|
||||
.required()
|
||||
.position(mp.getParameterIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.withTarget().method(bean, method);
|
||||
|
||||
CommandRegistration registration = builder.build();
|
||||
registry.register(registration);
|
||||
}
|
||||
}, method -> method.getAnnotation(ShellMethod.class) != null);
|
||||
}
|
||||
@@ -190,10 +263,4 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + " contributing "
|
||||
+ collectionToDelimitedString(commands.keySet(), ", ", "[", "]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,573 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2021 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
|
||||
*
|
||||
* https://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.shell.standard;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
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 javax.validation.Validator;
|
||||
import javax.validation.ValidatorFactory;
|
||||
import javax.validation.metadata.MethodDescriptor;
|
||||
import javax.validation.metadata.ParameterDescriptor;
|
||||
|
||||
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.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.ParameterMissingResolutionException;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.UnfinishedParameterResolutionException;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.ValueResult;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import static org.springframework.shell.Utils.unCamelify;
|
||||
|
||||
/**
|
||||
* Default ParameterResolver implementation that supports the following features:
|
||||
* <ul>
|
||||
* <li>named parameters (recognized because they start with some
|
||||
* {@link ShellMethod#prefix()})</li>
|
||||
* <li>implicit named parameters (from the actual method parameter name)</li>
|
||||
* <li>positional parameters (in order, for all parameter values that were not resolved
|
||||
* <i>via</i> named parameters)</li>
|
||||
* <li>default values (for all remaining parameters)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* 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).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 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 <i>via</i> {@link ShellOption} if
|
||||
* needed.
|
||||
* </p>
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
* @author Camilo Gonzalez
|
||||
*/
|
||||
@Component
|
||||
public class StandardParameterResolver implements ParameterResolver {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private Collection<ValueProvider> valueProviders = new HashSet<>();
|
||||
|
||||
private Validator validator = Utils.defaultValidator();
|
||||
|
||||
/**
|
||||
* 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<CacheKey, Map<Parameter, ParameterRawValue>> parameterCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
public StandardParameterResolver(ConversionService conversionService, Set<ValueProvider> valueProviders) {
|
||||
this.conversionService = conversionService;
|
||||
this.valueProviders = valueProviders;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setValidatorFactory(ValidatorFactory validatorFactory) {
|
||||
this.validator = validatorFactory.getValidator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter parameter) {
|
||||
boolean optOut = parameter.hasParameterAnnotation(ShellOption.class)
|
||||
&& parameter.getParameterAnnotation(ShellOption.class).optOut();
|
||||
return !optOut && parameter.getMethodAnnotation(ShellMethod.class) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueResult resolve(MethodParameter methodParameter, List<String> wordsBuffer) {
|
||||
List<String> words = wordsBuffer.stream().filter(w -> !w.isEmpty()).collect(Collectors.toList());
|
||||
|
||||
CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), wordsBuffer);
|
||||
parameterCache.clear();
|
||||
Map<Parameter, ParameterRawValue> resolved = parameterCache.computeIfAbsent(cacheKey, (k) -> {
|
||||
|
||||
Map<Parameter, ParameterRawValue> result = new HashMap<>();
|
||||
Map<String, String> namedParameters = new HashMap<>();
|
||||
|
||||
// index of words that haven't yet been used to resolve parameter values
|
||||
List<Integer> unusedWords = new ArrayList<>();
|
||||
|
||||
Set<String> possibleKeys = gatherAllPossibleKeys(methodParameter.getMethod());
|
||||
|
||||
// First, resolve all parameters passed by-name
|
||||
for (int i = 0; i < words.size(); i++) {
|
||||
int from = i;
|
||||
String word = words.get(i);
|
||||
if (possibleKeys.contains(word)) {
|
||||
String key = word;
|
||||
Parameter parameter = lookupParameterForKey(methodParameter.getMethod(), key);
|
||||
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)).findFirst().get(), 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);
|
||||
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, from, from));
|
||||
}
|
||||
else {
|
||||
i += arity;
|
||||
result.put(parameter, ParameterRawValue.explicit(raw, key, from, i));
|
||||
}
|
||||
} // store for later processing of positional params
|
||||
else {
|
||||
unusedWords.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 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<String> keys = getKeysForParameter(methodParameter.getMethod(), i)
|
||||
.collect(Collectors.toSet());
|
||||
Collection<String> 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) <= unusedWords.size()) {
|
||||
String raw = unusedWords.subList(offset, offset + arity).stream()
|
||||
.map(index -> words.get(index))
|
||||
.collect(Collectors.joining(","));
|
||||
int from = unusedWords.get(offset);
|
||||
int to = from + arity - 1;
|
||||
result.put(parameter, ParameterRawValue.explicit(raw, null, from, to));
|
||||
offset += arity;
|
||||
} // No more input. Try defaultValues
|
||||
else {
|
||||
Optional<String> defaultValue = defaultValueFor(parameter);
|
||||
defaultValue.ifPresent(
|
||||
value -> result.put(parameter, ParameterRawValue.implicit(value, null, null, null)));
|
||||
}
|
||||
}
|
||||
else if (copy.size() > 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Named parameter has been specified multiple times via " + quote(copy));
|
||||
}
|
||||
}
|
||||
|
||||
Assert.isTrue(offset == unusedWords.size(),
|
||||
"Too many arguments: the following could not be mapped to parameters: "
|
||||
+ unusedWords.subList(offset, unusedWords.size()).stream()
|
||||
.map(index -> words.get(index)).collect(Collectors.joining(" ", "'", "'")));
|
||||
return result;
|
||||
});
|
||||
|
||||
Parameter param = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()];
|
||||
if (!resolved.containsKey(param)) {
|
||||
throw new ParameterMissingResolutionException(describe(methodParameter).findFirst().get());
|
||||
}
|
||||
ParameterRawValue parameterRawValue = resolved.get(param);
|
||||
Object value = convertRawValue(parameterRawValue, methodParameter);
|
||||
BitSet wordsUsed = getWordsUsed(parameterRawValue);
|
||||
BitSet wordsUsedForValue = getWordsUsedForValue(parameterRawValue);
|
||||
return new ValueResult(methodParameter, value, wordsUsed, wordsUsedForValue);
|
||||
}
|
||||
|
||||
private BitSet getWordsUsed(ParameterRawValue parameterRawValue) {
|
||||
if (parameterRawValue.from != null) {
|
||||
BitSet wordsUsed = new BitSet();
|
||||
wordsUsed.set(parameterRawValue.from, parameterRawValue.to + 1);
|
||||
return wordsUsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private BitSet getWordsUsedForValue(ParameterRawValue parameterRawValue) {
|
||||
if (parameterRawValue.from != null) {
|
||||
BitSet wordsUsedForValue = new BitSet();
|
||||
wordsUsedForValue.set(parameterRawValue.from, parameterRawValue.to + 1);
|
||||
if (parameterRawValue.key != null) {
|
||||
wordsUsedForValue.clear(parameterRawValue.from);
|
||||
}
|
||||
return wordsUsedForValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object convertRawValue(ParameterRawValue parameterRawValue, MethodParameter methodParameter) {
|
||||
String s = parameterRawValue.value;
|
||||
if (ShellOption.NULL.equals(s)) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return conversionService.convert(s, TypeDescriptor.valueOf(String.class),
|
||||
new TypeDescriptor(methodParameter));
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> gatherAllPossibleKeys(Method method) {
|
||||
return Arrays.stream(method.getParameters())
|
||||
.flatMap(this::getKeysForParameter)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private String prefixForMethod(Executable method) {
|
||||
return method.getAnnotation(ShellMethod.class).prefix();
|
||||
}
|
||||
|
||||
private Optional<String> defaultValueFor(Parameter parameter) {
|
||||
ShellOption option = parameter.getAnnotation(ShellOption.class);
|
||||
if (option != null && !ShellOption.NONE.equals(option.defaultValue())) {
|
||||
return Optional.of(option.defaultValue());
|
||||
}
|
||||
else if (getArity(parameter) == 0) {
|
||||
return Optional.of("false");
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private boolean booleanDefaultValue(Parameter parameter) {
|
||||
ShellOption option = parameter.getAnnotation(ShellOption.class);
|
||||
if (option != null && !ShellOption.NONE.equals(option.defaultValue())) {
|
||||
return Boolean.parseBoolean(option.defaultValue());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<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<String> defaultValue = defaultValueFor(jlrParameter);
|
||||
if (defaultValue.isPresent()) {
|
||||
result.defaultValue(defaultValue.map(dv -> dv.equals(ShellOption.NULL) ? "<none>" : dv).get());
|
||||
}
|
||||
}
|
||||
result
|
||||
.keys(getKeysForParameter(parameter.getMethod(), parameter.getParameterIndex())
|
||||
.collect(Collectors.toList()))
|
||||
.mandatoryKey(false);
|
||||
|
||||
MethodDescriptor constraintsForMethod = validator.getConstraintsForClass(parameter.getDeclaringClass())
|
||||
.getConstraintsForMethod(parameter.getMethod().getName(), parameter.getMethod().getParameterTypes());
|
||||
if (constraintsForMethod != null) {
|
||||
ParameterDescriptor constraintsDescriptor = constraintsForMethod
|
||||
.getParameterDescriptors().get(parameter.getParameterIndex());
|
||||
result.elementDescriptor(constraintsDescriptor);
|
||||
}
|
||||
|
||||
return Stream.of(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompletionProposal> 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;
|
||||
int arity = 1;
|
||||
try {
|
||||
resolve(methodParameter, context.getWords());
|
||||
CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), context.getWords());
|
||||
Parameter parameter = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()];
|
||||
arity = getArity(parameter);
|
||||
parameterRawValue = parameterCache.get(cacheKey).get(parameter);
|
||||
set = parameterRawValue.explicit;
|
||||
}
|
||||
catch (ParameterMissingResolutionException e) {
|
||||
set = false;
|
||||
}
|
||||
catch (UnfinishedParameterResolutionException e) {
|
||||
if (e.getParameterDescription().parameter().equals(methodParameter)) {
|
||||
unfinished = e;
|
||||
set = false;
|
||||
}
|
||||
else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Most likely what is already typed would fail resolution (eg type conversion failure)
|
||||
return argumentKeysThatStartWithContextPrefix(methodParameter, context);
|
||||
}
|
||||
|
||||
// There are 4 possible cases:
|
||||
// 1) parameter not set at all
|
||||
// 2) parameter set via its key, not enough input to consume a value
|
||||
// 3) parameter set with multiple values, enough to cover arity. We're done
|
||||
// 4) parameter set, and some value bound. But maybe that value is just a prefix to what
|
||||
// the user actually wants
|
||||
// 4.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 argumentKeysThatStartWithContextPrefix(methodParameter, context);
|
||||
} // case 2
|
||||
else {
|
||||
return valueCompletions(methodParameter, context);
|
||||
}
|
||||
}
|
||||
else {
|
||||
List<CompletionProposal> result = new ArrayList<>();
|
||||
|
||||
Object value = convertRawValue(parameterRawValue, methodParameter);
|
||||
if (value instanceof Collection && ((Collection<?>) value).size() == arity
|
||||
|| (ObjectUtils.isArray(value) && Array.getLength(value) == arity)) {
|
||||
// We're done already
|
||||
return result;
|
||||
}
|
||||
if (!context.currentWord().equals("")) {
|
||||
// Case 4
|
||||
result.addAll(valueCompletions(methodParameter, context));
|
||||
}
|
||||
|
||||
if (parameterRawValue.positional()) {
|
||||
// Case 4.1: There exists "--command foo" and user has typed "--comm" which (wrongly) got
|
||||
// resolved as a positional param
|
||||
result.addAll(argumentKeysThatStartWithContextPrefix(methodParameter, context));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private List<CompletionProposal> 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<CompletionProposal> argumentKeysThatStartWithContextPrefix(MethodParameter methodParameter,
|
||||
CompletionContext context) {
|
||||
String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : "";
|
||||
return describe(methodParameter).flatMap(pd -> pd.keys().stream())
|
||||
.filter(k -> k.startsWith(prefix))
|
||||
.map(CompletionProposal::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of {@code foo[] or Collection<Foo>} 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<String> 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() != ShellOption.ARITY_USE_HEURISTICS ? option.arity() : inferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key(s) for the i-th parameter of the command method, resolved either from
|
||||
* the {@link ShellOption} annotation, or from the actual parameter name.
|
||||
*/
|
||||
private Stream<String> getKeysForParameter(Method method, int index) {
|
||||
Parameter p = method.getParameters()[index];
|
||||
return getKeysForParameter(p);
|
||||
}
|
||||
|
||||
private Stream<String> getKeysForParameter(Parameter p) {
|
||||
Executable method = p.getDeclaringExecutable();
|
||||
String prefix = prefixForMethod(method);
|
||||
ShellOption option = p.getAnnotation(ShellOption.class);
|
||||
if (option != null && option.value().length > 0) {
|
||||
return Arrays.stream(option.value());
|
||||
}
|
||||
else {
|
||||
return Stream.of(prefix + Utils.unCamelify(Utils.createMethodParameter(p).getParameterName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method parameter that should be bound to the given key.
|
||||
*/
|
||||
private Parameter lookupParameterForKey(Method method, String key) {
|
||||
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' in %s", key, method));
|
||||
}
|
||||
|
||||
private static class CacheKey {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final List<String> words;
|
||||
|
||||
private CacheKey(Method method, List<String> 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return method.getName() + " " + words;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParameterRawValue {
|
||||
|
||||
private Integer from;
|
||||
|
||||
private Integer to;
|
||||
|
||||
/**
|
||||
* 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, Integer from, Integer to) {
|
||||
this.value = value;
|
||||
this.explicit = explicit;
|
||||
this.key = key;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
public static ParameterRawValue explicit(String value, String key, Integer from, Integer to) {
|
||||
return new ParameterRawValue(value, true, key, from, to);
|
||||
}
|
||||
|
||||
public static ParameterRawValue implicit(String value, String key, Integer from, Integer to) {
|
||||
return new ParameterRawValue(value, false, key, from, to);
|
||||
}
|
||||
|
||||
public boolean positional() {
|
||||
return key == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ParameterRawValue{" +
|
||||
"value='" + value + '\'' +
|
||||
", explicit=" + explicit +
|
||||
", key='" + key + '\'' +
|
||||
", from=" + from +
|
||||
", to=" + to +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,10 +22,10 @@ import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@@ -36,11 +36,8 @@ import org.stringtemplate.v4.STGroupString;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -54,14 +51,11 @@ import org.springframework.util.MultiValueMap;
|
||||
public abstract class AbstractCompletions {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final CommandRegistry commandRegistry;
|
||||
private final List<ParameterResolver> parameterResolvers;
|
||||
private final CommandCatalog commandCatalog;
|
||||
|
||||
public AbstractCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
public AbstractCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.commandRegistry = commandRegistry;
|
||||
this.parameterResolvers = parameterResolvers;
|
||||
this.commandCatalog = commandCatalog;
|
||||
}
|
||||
|
||||
protected Builder builder() {
|
||||
@@ -74,12 +68,12 @@ public abstract class AbstractCompletions {
|
||||
* all needed to build completions structure.
|
||||
*/
|
||||
protected CommandModel generateCommandModel() {
|
||||
Map<String, MethodTarget> commandsByName = commandRegistry.listCommands();
|
||||
Collection<CommandRegistration> commandsByName = commandCatalog.getRegistrations().values();
|
||||
HashMap<String, DefaultCommandModelCommand> commands = new HashMap<>();
|
||||
HashSet<CommandModelCommand> topCommands = new HashSet<>();
|
||||
commandsByName.entrySet().stream()
|
||||
.forEach(entry -> {
|
||||
String key = entry.getKey();
|
||||
commandsByName.stream()
|
||||
.forEach(registration -> {
|
||||
String key = registration.getCommand();
|
||||
String[] splitKeys = key.split(" ");
|
||||
String commandKey = "";
|
||||
for (int i = 0; i < splitKeys.length; i++) {
|
||||
@@ -94,12 +88,13 @@ public abstract class AbstractCompletions {
|
||||
}
|
||||
DefaultCommandModelCommand command = commands.computeIfAbsent(commandKey,
|
||||
(fullCommand) -> new DefaultCommandModelCommand(fullCommand, main));
|
||||
MethodTarget methodTarget = entry.getValue();
|
||||
List<ParameterDescription> parameterDescriptions = getParameterDescriptions(methodTarget);
|
||||
List<DefaultCommandModelOption> options = parameterDescriptions.stream()
|
||||
.flatMap(pd -> pd.keys().stream())
|
||||
.map(k -> new DefaultCommandModelOption(k))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// TODO long vs short
|
||||
List<CommandModelOption> options = registration.getOptions().stream()
|
||||
.flatMap(co -> Arrays.stream(co.getLongNames()))
|
||||
.map(lo -> CommandModelOption.of("--", lo))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (i == splitKeys.length - 1) {
|
||||
command.addOptions(options);
|
||||
}
|
||||
@@ -114,13 +109,6 @@ public abstract class AbstractCompletions {
|
||||
return new DefaultCommandModel(new ArrayList<>(topCommands));
|
||||
}
|
||||
|
||||
private List<ParameterDescription> getParameterDescriptions(MethodTarget methodTarget) {
|
||||
return Utils.createMethodParameters(methodTarget.getMethod())
|
||||
.flatMap(mp -> parameterResolvers.stream().filter(pr -> pr.supports(mp)).limit(1L)
|
||||
.flatMap(pr -> pr.describe(mp)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a command model structure. Is also used as entry model
|
||||
* for ST4 templates which is a reason it has utility methods for easier usage
|
||||
@@ -208,6 +196,10 @@ public abstract class AbstractCompletions {
|
||||
|
||||
interface CommandModelOption {
|
||||
String option();
|
||||
|
||||
static CommandModelOption of(String prefix, String name) {
|
||||
return new DefaultCommandModelOption(String.format("%s%s", prefix, name));
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultCommandModel implements CommandModel {
|
||||
@@ -294,7 +286,7 @@ public abstract class AbstractCompletions {
|
||||
return options;
|
||||
}
|
||||
|
||||
void addOptions(List<DefaultCommandModelOption> options) {
|
||||
void addOptions(List<CommandModelOption> options) {
|
||||
this.options.addAll(options);
|
||||
}
|
||||
|
||||
@@ -341,7 +333,7 @@ public abstract class AbstractCompletions {
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultCommandModelOption implements CommandModelOption {
|
||||
static class DefaultCommandModelOption implements CommandModelOption {
|
||||
|
||||
private String option;
|
||||
|
||||
|
||||
@@ -15,11 +15,8 @@
|
||||
*/
|
||||
package org.springframework.shell.standard.completion;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
|
||||
/**
|
||||
* Completion script generator for a {@code bash}.
|
||||
@@ -28,9 +25,8 @@ import org.springframework.shell.ParameterResolver;
|
||||
*/
|
||||
public class BashCompletions extends AbstractCompletions {
|
||||
|
||||
public BashCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
super(resourceLoader, commandRegistry, parameterResolvers);
|
||||
public BashCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
|
||||
super(resourceLoader, commandCatalog);
|
||||
}
|
||||
|
||||
public String generate(String rootCommand) {
|
||||
|
||||
@@ -29,11 +29,11 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -47,7 +47,7 @@ import static org.mockito.Mockito.when;
|
||||
public class CommandValueProviderTest {
|
||||
|
||||
@Mock
|
||||
private CommandRegistry shell;
|
||||
private CommandCatalog catalog;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
@@ -56,7 +56,7 @@ public class CommandValueProviderTest {
|
||||
|
||||
@Test
|
||||
public void testValues() {
|
||||
CommandValueProvider valueProvider = new CommandValueProvider(shell);
|
||||
CommandValueProvider valueProvider = new CommandValueProvider(catalog);
|
||||
|
||||
Method help = ReflectionUtils.findMethod(Command.class, "help", String.class);
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(help, 0);
|
||||
@@ -64,12 +64,12 @@ public class CommandValueProviderTest {
|
||||
boolean supports = valueProvider.supports(methodParameter, completionContext);
|
||||
|
||||
assertThat(supports).isEqualTo(true);
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("me", null);
|
||||
registrations.put("meow", null);
|
||||
registrations.put("yourself", null);
|
||||
|
||||
Map<String, MethodTarget> commands = new HashMap<>();
|
||||
commands.put("me", null);
|
||||
commands.put("meow", null);
|
||||
commands.put("yourself", null);
|
||||
when(shell.listCommands()).thenReturn(commands);
|
||||
when(catalog.getRegistrations()).thenReturn(registrations);
|
||||
List<CompletionProposal> proposals = valueProvider.complete(methodParameter, completionContext, new String[0]);
|
||||
|
||||
assertThat(proposals).extracting("value", String.class)
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017-2022 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
|
||||
*
|
||||
* https://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.shell.standard;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.standard.test1.GroupOneCommands;
|
||||
import org.springframework.shell.standard.test2.GroupThreeCommands;
|
||||
import org.springframework.shell.standard.test2.GroupTwoCommands;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StandardMethodTargetRegistrar}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class StandardMethodTargetRegistrarTest {
|
||||
|
||||
private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
|
||||
private ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
|
||||
@Test
|
||||
public void testRegistrations() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Sample.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
|
||||
MethodTarget methodTarget = registry.listCommands().get("say-hello");
|
||||
assertThat(methodTarget).isNotNull();
|
||||
assertThat(methodTarget.getHelp()).isEqualTo("some command");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "sayHello", String.class));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
|
||||
methodTarget = registry.listCommands().get("hi");
|
||||
assertThat(methodTarget).isNotNull();
|
||||
assertThat(methodTarget.getHelp()).isEqualTo("method with alias");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "greet", String.class));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
methodTarget = registry.listCommands().get("alias");
|
||||
assertThat(methodTarget).isNotNull();
|
||||
assertThat(methodTarget.getHelp()).isEqualTo("method with alias");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "greet", String.class));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class Sample {
|
||||
|
||||
@ShellMethod("some command")
|
||||
public String sayHello(String what) {
|
||||
return "hello " + what;
|
||||
}
|
||||
|
||||
@ShellMethod(value = "method with alias", key = {"hi", "alias"})
|
||||
public String greet(String what) {
|
||||
return "hi " + what;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicators() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class);
|
||||
|
||||
MethodTarget methodTarget = registry.listCommands().get("say-hello");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "sayHello"));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
|
||||
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("sayHelloAvailability");
|
||||
sample.available = true;
|
||||
|
||||
methodTarget = registry.listCommands().get("hi");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "hi"));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
|
||||
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("customAvailabilityMethod");
|
||||
sample.available = true;
|
||||
|
||||
methodTarget = registry.listCommands().get("bonjour");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "bonjour"));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
|
||||
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("availabilityForSeveralCommands");
|
||||
sample.available = true;
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class SampleWithAvailability {
|
||||
|
||||
private boolean available = true;
|
||||
|
||||
@ShellMethod("some command with an implicit availability indicator")
|
||||
public void sayHello() {
|
||||
|
||||
}
|
||||
public Availability sayHelloAvailability() {
|
||||
return available ? Availability.available() : Availability.unavailable("sayHelloAvailability");
|
||||
}
|
||||
|
||||
|
||||
@ShellMethodAvailability("customAvailabilityMethod")
|
||||
@ShellMethod("some method with an explicit availability indicator")
|
||||
public void hi() {
|
||||
|
||||
}
|
||||
public Availability customAvailabilityMethod() {
|
||||
return available ? Availability.available() : Availability.unavailable("customAvailabilityMethod");
|
||||
}
|
||||
|
||||
@ShellMethod(value = "some method with an explicit availability indicator", key = {"bonjour", "salut"})
|
||||
public void bonjour() {
|
||||
|
||||
}
|
||||
@ShellMethodAvailability({"salut", "other"})
|
||||
public Availability availabilityForSeveralCommands() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityForSeveralCommands");
|
||||
}
|
||||
|
||||
|
||||
@ShellMethod("a command whose availability indicator will come from wildcard")
|
||||
public void wild() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethodAvailability("*")
|
||||
private Availability availabilityFromWildcard() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityFromWildcard");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorErrorMultipleExplicit() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(registry);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("When set on a @ShellMethod method, the value of the @ShellMethodAvailability should be a single element")
|
||||
.hasMessageContaining("Found [one, two]")
|
||||
.hasMessageContaining("wrong()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorOnShellMethod {
|
||||
|
||||
@ShellMethodAvailability({"one", "two"})
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorWildcardNotAlone() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(registry);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("When using '*' as a wildcard for ShellMethodAvailability, this can be the only value. Found [one, *]")
|
||||
.hasMessageContaining("availability()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorWildcardNotAlone {
|
||||
|
||||
@ShellMethodAvailability({"one", "*"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorAmbiguous() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(registry);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Found several @ShellMethodAvailability")
|
||||
.hasMessageContaining("wrong()")
|
||||
.hasMessageContaining("availability()")
|
||||
.hasMessageContaining("otherAvailability()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorAmbiguous {
|
||||
|
||||
@ShellMethodAvailability({"one", "wrong"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethodAvailability({"bar", "wrong"})
|
||||
public Availability otherAvailability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrouping() {
|
||||
ApplicationContext context = new AnnotationConfigApplicationContext(GroupOneCommands.class,
|
||||
GroupTwoCommands.class, GroupThreeCommands.class);
|
||||
registrar.setApplicationContext(context);
|
||||
registrar.register(registry);
|
||||
|
||||
Map<String, MethodTarget> commands = registry.listCommands();
|
||||
Assertions.assertThat(commands.get("explicit1").getGroup()).isEqualTo("Explicit Group Method Level 1");
|
||||
Assertions.assertThat(commands.get("explicit2").getGroup()).isEqualTo("Explicit Group Method Level 2");
|
||||
Assertions.assertThat(commands.get("explicit3").getGroup()).isEqualTo("Explicit Group Method Level 3");
|
||||
Assertions.assertThat(commands.get("implicit1").getGroup()).isEqualTo("Implicit Group Package Level 1");
|
||||
Assertions.assertThat(commands.get("implicit2").getGroup()).isEqualTo("Group Two Commands");
|
||||
Assertions.assertThat(commands.get("implicit3").getGroup()).isEqualTo("Explicit Group 3 Class Level");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteractionModeInteractive() {
|
||||
DefaultShellContext shellContext = new DefaultShellContext();
|
||||
shellContext.setInteractionMode(InteractionMode.INTERACTIVE);
|
||||
registry = new ConfigurableCommandRegistry(shellContext);
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
|
||||
assertThat(registry.listCommands().get("foo1")).isNotNull();
|
||||
assertThat(registry.listCommands().get("foo2")).isNull();
|
||||
assertThat(registry.listCommands().get("foo3")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteractionModeNonInteractive() {
|
||||
DefaultShellContext shellContext = new DefaultShellContext();
|
||||
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
|
||||
registry = new ConfigurableCommandRegistry(shellContext);
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
|
||||
assertThat(registry.listCommands().get("foo1")).isNull();
|
||||
assertThat(registry.listCommands().get("foo2")).isNotNull();
|
||||
assertThat(registry.listCommands().get("foo3")).isNotNull();
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class InteractionModeCommands {
|
||||
|
||||
@ShellMethod(value = "foo1", interactionMode = InteractionMode.INTERACTIVE)
|
||||
public void foo1() {
|
||||
}
|
||||
|
||||
@ShellMethod(value = "foo2", interactionMode = InteractionMode.NONINTERACTIVE)
|
||||
public void foo2() {
|
||||
}
|
||||
|
||||
@ShellMethod(value = "foo3")
|
||||
public void foo3() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* Copyright 2017-2022 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
|
||||
*
|
||||
* https://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.shell.standard;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.standard.test1.GroupOneCommands;
|
||||
import org.springframework.shell.standard.test2.GroupThreeCommands;
|
||||
import org.springframework.shell.standard.test2.GroupTwoCommands;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StandardMethodTargetRegistrar}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class StandardMethodTargetRegistrarTests {
|
||||
|
||||
private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
|
||||
private AnnotationConfigApplicationContext applicationContext;
|
||||
private CommandCatalog catalog;
|
||||
private DefaultShellContext shellContext;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
shellContext = new DefaultShellContext();
|
||||
catalog = CommandCatalog.of(null, shellContext);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
if (applicationContext != null) {
|
||||
applicationContext.close();
|
||||
}
|
||||
applicationContext = null;
|
||||
catalog = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrations() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(Sample.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
|
||||
assertThat(registrations).hasSize(3);
|
||||
|
||||
assertThat(registrations.get("say-hello")).isNotNull();
|
||||
assertThat(registrations.get("say-hello").getAvailability()).isNotNull();
|
||||
assertThat(registrations.get("say-hello").getOptions()).hasSize(1);
|
||||
assertThat(registrations.get("say-hello").getOptions().get(0).getLongNames()).containsExactly("what");
|
||||
|
||||
assertThat(registrations.get("hi")).isNotNull();
|
||||
assertThat(registrations.get("hi").getAvailability()).isNotNull();
|
||||
|
||||
assertThat(registrations.get("alias")).isNotNull();
|
||||
assertThat(registrations.get("alias").getAvailability()).isNotNull();
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class Sample {
|
||||
|
||||
@ShellMethod("some command")
|
||||
public String sayHello(String what) {
|
||||
return "hello " + what;
|
||||
}
|
||||
|
||||
@ShellMethod(value = "method with alias", key = {"hi", "alias"})
|
||||
public String greet(String what) {
|
||||
return "hi " + what;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicators() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class);
|
||||
SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
|
||||
|
||||
assertThat(registrations.get("say-hello")).isNotNull();
|
||||
assertThat(registrations.get("say-hello").getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(registrations.get("say-hello").getAvailability().isAvailable()).isFalse();
|
||||
assertThat(registrations.get("say-hello").getAvailability().getReason()).isEqualTo("sayHelloAvailability");
|
||||
sample.available = true;
|
||||
|
||||
assertThat(registrations.get("hi")).isNotNull();
|
||||
assertThat(registrations.get("hi").getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(registrations.get("hi").getAvailability().isAvailable()).isFalse();
|
||||
assertThat(registrations.get("hi").getAvailability().getReason()).isEqualTo("customAvailabilityMethod");
|
||||
sample.available = true;
|
||||
|
||||
assertThat(registrations.get("bonjour")).isNotNull();
|
||||
assertThat(registrations.get("bonjour").getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(registrations.get("bonjour").getAvailability().isAvailable()).isFalse();
|
||||
assertThat(registrations.get("bonjour").getAvailability().getReason()).isEqualTo("availabilityForSeveralCommands");
|
||||
sample.available = true;
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class SampleWithAvailability {
|
||||
|
||||
private boolean available = true;
|
||||
|
||||
@ShellMethod("some command with an implicit availability indicator")
|
||||
public void sayHello() {
|
||||
|
||||
}
|
||||
public Availability sayHelloAvailability() {
|
||||
return available ? Availability.available() : Availability.unavailable("sayHelloAvailability");
|
||||
}
|
||||
|
||||
|
||||
@ShellMethodAvailability("customAvailabilityMethod")
|
||||
@ShellMethod("some method with an explicit availability indicator")
|
||||
public void hi() {
|
||||
|
||||
}
|
||||
public Availability customAvailabilityMethod() {
|
||||
return available ? Availability.available() : Availability.unavailable("customAvailabilityMethod");
|
||||
}
|
||||
|
||||
@ShellMethod(value = "some method with an explicit availability indicator", key = {"bonjour", "salut"})
|
||||
public void bonjour() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethodAvailability({"salut", "other"})
|
||||
public Availability availabilityForSeveralCommands() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityForSeveralCommands");
|
||||
}
|
||||
|
||||
@ShellMethod("a command whose availability indicator will come from wildcard")
|
||||
public void wild() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethodAvailability("*")
|
||||
private Availability availabilityFromWildcard() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityFromWildcard");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorErrorMultipleExplicit() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(catalog);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("When set on a @ShellMethod method, the value of the @ShellMethodAvailability should be a single element")
|
||||
.hasMessageContaining("Found [one, two]")
|
||||
.hasMessageContaining("wrong()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorOnShellMethod {
|
||||
|
||||
@ShellMethodAvailability({"one", "two"})
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorWildcardNotAlone() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(catalog);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("When using '*' as a wildcard for ShellMethodAvailability, this can be the only value. Found [one, *]")
|
||||
.hasMessageContaining("availability()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorWildcardNotAlone {
|
||||
|
||||
@ShellMethodAvailability({"one", "*"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorAmbiguous() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(catalog);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Found several @ShellMethodAvailability")
|
||||
.hasMessageContaining("wrong()")
|
||||
.hasMessageContaining("availability()")
|
||||
.hasMessageContaining("otherAvailability()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorAmbiguous {
|
||||
|
||||
@ShellMethodAvailability({"one", "wrong"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethodAvailability({"bar", "wrong"})
|
||||
public Availability otherAvailability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrouping() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(GroupOneCommands.class,
|
||||
GroupTwoCommands.class, GroupThreeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
|
||||
assertThat(catalog.getRegistrations().get("explicit1")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 1");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("explicit2")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 2");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("explicit3")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 3");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("implicit1")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Implicit Group Package Level 1");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("implicit2")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Group Two Commands");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("implicit3")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Explicit Group 3 Class Level");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteractionModeInteractive() {
|
||||
shellContext.setInteractionMode(InteractionMode.INTERACTIVE);
|
||||
applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
|
||||
assertThat(catalog.getRegistrations().get("foo1")).isNotNull();
|
||||
assertThat(catalog.getRegistrations().get("foo2")).isNull();
|
||||
assertThat(catalog.getRegistrations().get("foo3")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteractionModeNonInteractive() {
|
||||
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
|
||||
applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
|
||||
assertThat(catalog.getRegistrations().get("foo1")).isNull();
|
||||
assertThat(catalog.getRegistrations().get("foo2")).isNotNull();
|
||||
assertThat(catalog.getRegistrations().get("foo3")).isNotNull();
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class InteractionModeCommands {
|
||||
|
||||
@ShellMethod(value = "foo1", interactionMode = InteractionMode.INTERACTIVE)
|
||||
public void foo1() {
|
||||
}
|
||||
|
||||
@ShellMethod(value = "foo2", interactionMode = InteractionMode.NONINTERACTIVE)
|
||||
public void foo2() {
|
||||
}
|
||||
|
||||
@ShellMethod(value = "foo3")
|
||||
public void foo3() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,272 +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
|
||||
*
|
||||
* https://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.shell.standard;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.reader.ParsedLine;
|
||||
import org.jline.reader.impl.DefaultParser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.ParameterMissingResolutionException;
|
||||
import org.springframework.shell.UnfinishedParameterResolutionException;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.ValueResult;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.shell.ValueResultAsserts.assertThat;
|
||||
import static org.springframework.util.ReflectionUtils.findMethod;
|
||||
|
||||
/**
|
||||
* Unit tests for DefaultParameterResolver.
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
*/
|
||||
public class StandardParameterResolverTest {
|
||||
|
||||
// private StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(), Collections.emptySet());
|
||||
|
||||
// Tests for resolution
|
||||
|
||||
@Test
|
||||
public void testParses() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
List<String> words = asList("--force --name --foo y".split(" "));
|
||||
ValueResult result0 = resolver.resolve(Utils.createMethodParameter(method, 0), words);
|
||||
assertThat(result0).hasValue(true).usesWords(0).notUsesWordsForValue();
|
||||
assertThat(result0.wordsUsed(words)).containsExactly("--force");
|
||||
|
||||
ValueResult result1 = resolver.resolve(Utils.createMethodParameter(method, 1), words);
|
||||
assertThat(result1).hasValue("--foo").usesWords(1, 2).usesWordsForValue(2);
|
||||
assertThat(result1.wordsUsed(words)).containsExactly("--name", "--foo");
|
||||
assertThat(result1.wordsUsedForValue(words)).containsExactly("--foo");
|
||||
|
||||
ValueResult result2 = resolver.resolve(Utils.createMethodParameter(method, 2), words);
|
||||
assertThat(result2).hasValue("y").usesWords(3).usesWordsForValue(3);
|
||||
assertThat(result2.wordsUsed(words)).containsExactly("y");
|
||||
|
||||
ValueResult result3 = resolver.resolve(Utils.createMethodParameter(method, 3), words);
|
||||
assertThat(result3).hasValue("last").notUsesWords().notUsesWordsForValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParsesWithMethodPrefix() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "prefixTest", String.class);
|
||||
|
||||
ValueResult result = resolver.resolve(Utils.createMethodParameter(method, 0),
|
||||
asList("-message abc".split(" ")));
|
||||
assertThat(result).hasValue("abc").usesWords(0, 1).usesWordsForValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameterSpecifiedTwiceViaDifferentAliases() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--force --name --foo y --bar x --baz z".split(" ")));
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Named parameter has been specified multiple times via '--bar, --baz'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameterSpecifiedTwiceViaSameKey() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--force --name --foo y --baz x --baz z".split(" ")));
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Parameter for '--baz' has already been specified");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTooMuchInput() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--foo hello --name bar --force --bar well leftover".split(" ")));
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("the following could not be mapped to parameters: 'leftover'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncompleteCommandResolution() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "shutdown", Remote.Delay.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--delay".split(" ")));
|
||||
}).isInstanceOf(UnfinishedParameterResolutionException.class)
|
||||
.hasMessageContaining("Error trying to resolve '--delay delay' using [--delay]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncompleteCommandResolutionBigArity() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "add", List.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--numbers 1 2".split(" ")));
|
||||
}).isInstanceOf(UnfinishedParameterResolutionException.class)
|
||||
.hasMessageContaining("Error trying to resolve '--numbers list list list' using [--numbers 1 2]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnresolvableArg() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 1),
|
||||
asList("--foo hello --force --bar well".split(" ")));
|
||||
}).isInstanceOf(ParameterMissingResolutionException.class)
|
||||
.hasMessageContaining("Parameter '--name string' should be specified");
|
||||
}
|
||||
|
||||
// Tests for completion
|
||||
|
||||
@Test
|
||||
public void testParameterKeyNotYetSetAppearsInProposals() {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
List<String> 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() {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
List<String> 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() {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
List<String> 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());
|
||||
assertThat(completions).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotTheRightTimeToCompleteThatParameter() {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "shutdown", Remote.Delay.class);
|
||||
List<String> completions = resolver.complete(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
contextFor("--delay 323")
|
||||
).stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValueCompletionWithNonDefaultArity() {
|
||||
Set<ValueProvider> valueProviders = new HashSet<>();
|
||||
valueProviders.add(new Remote.NumberValueProvider("12", "42", "7"));
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
valueProviders);
|
||||
|
||||
Method[] methods = {
|
||||
findMethod(org.springframework.shell.standard.Remote.class, "add", List.class),
|
||||
findMethod(org.springframework.shell.standard.Remote.class, "addAsArray", int[].class),
|
||||
};
|
||||
for (Method method : methods) {
|
||||
List<String> completions = resolver
|
||||
.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers ")).stream()
|
||||
.map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("12", "42", "7");
|
||||
|
||||
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 "))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("12", "7");
|
||||
|
||||
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 34 "))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("12", "7");
|
||||
|
||||
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 34 66 "))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).isEmpty(); // All 3 have already been set
|
||||
}
|
||||
}
|
||||
|
||||
private CompletionContext contextFor(String input) {
|
||||
DefaultParser defaultParser = new DefaultParser();
|
||||
ParsedLine parsed = defaultParser.parse(input, input.length());
|
||||
List<String> words = parsed.words().stream().filter(w -> w.length() > 0).collect(Collectors.toList());
|
||||
|
||||
return new CompletionContext(words, parsed.wordIndex(), parsed.wordCursor());
|
||||
}
|
||||
}
|
||||
@@ -15,26 +15,15 @@
|
||||
*/
|
||||
package org.springframework.shell.standard.completion;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
import org.springframework.shell.standard.StandardParameterResolver;
|
||||
import org.springframework.shell.standard.completion.AbstractCompletions.CommandModel;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -43,30 +32,50 @@ public class AbstractCompletionsTests {
|
||||
@Test
|
||||
public void testBasicModelGeneration() {
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
parameterResolvers.add(resolver);
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
|
||||
TestCommands commands = new TestCommands();
|
||||
|
||||
Method method1 = ReflectionUtils.findMethod(TestCommands.class, "test1", String.class);
|
||||
Method method2 = ReflectionUtils.findMethod(TestCommands.class, "test2");
|
||||
Method method3 = ReflectionUtils.findMethod(TestCommands.class, "test3");
|
||||
Method method4 = ReflectionUtils.findMethod(TestCommands.class, "test4", String.class);
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("test1")
|
||||
.withTarget()
|
||||
.method(commands, "test1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("param1")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
MethodTarget methodTarget1 = new MethodTarget(method1, commands, "help");
|
||||
MethodTarget methodTarget2 = new MethodTarget(method2, commands, "help");
|
||||
MethodTarget methodTarget3 = new MethodTarget(method3, commands, "help");
|
||||
MethodTarget methodTarget4 = new MethodTarget(method4, commands, "help");
|
||||
CommandRegistration registration2 = CommandRegistration.builder()
|
||||
.command("test2")
|
||||
.withTarget()
|
||||
.method(commands, "test2")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
commandRegistry.register("test1", methodTarget1);
|
||||
commandRegistry.register("test2", methodTarget2);
|
||||
commandRegistry.register("test3", methodTarget3);
|
||||
commandRegistry.register("test3 test4", methodTarget4);
|
||||
CommandRegistration registration3 = CommandRegistration.builder()
|
||||
.command("test3")
|
||||
.withTarget()
|
||||
.method(commands, "test3")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandRegistry, parameterResolvers);
|
||||
CommandRegistration registration4 = CommandRegistration.builder()
|
||||
.command("test3", "test4")
|
||||
.withTarget()
|
||||
.method(commands, "test4")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("param4")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
commandCatalog.register(registration1);
|
||||
commandCatalog.register(registration2);
|
||||
commandCatalog.register(registration3);
|
||||
commandCatalog.register(registration4);
|
||||
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandCatalog);
|
||||
CommandModel commandModel = completions.testCommandModel();
|
||||
assertThat(commandModel.getCommands()).hasSize(3);
|
||||
assertThat(commandModel.getCommands().stream().map(c -> c.getMainCommand())).containsExactlyInAnyOrder("test1", "test2",
|
||||
@@ -92,9 +101,8 @@ public class AbstractCompletionsTests {
|
||||
@Test
|
||||
public void testBuilder() {
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandRegistry, parameterResolvers);
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandCatalog);
|
||||
|
||||
String result = completions.testBuilder()
|
||||
.attribute("x", "command")
|
||||
@@ -106,9 +114,8 @@ public class AbstractCompletionsTests {
|
||||
|
||||
private static class TestCompletions extends AbstractCompletions {
|
||||
|
||||
public TestCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
super(resourceLoader, commandRegistry, parameterResolvers);
|
||||
public TestCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
|
||||
super(resourceLoader, commandCatalog);
|
||||
}
|
||||
|
||||
CommandModel testCommandModel() {
|
||||
|
||||
@@ -15,17 +15,16 @@
|
||||
*/
|
||||
package org.springframework.shell.standard.completion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandContext;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -48,11 +47,71 @@ public class BashCompletionsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotError() {
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
BashCompletions completions = new BashCompletions(context, commandRegistry, parameterResolvers);
|
||||
public void testNoCommands() {
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
BashCompletions completions = new BashCompletions(context, commandCatalog);
|
||||
String bash = completions.generate("root-command");
|
||||
assertThat(bash).contains("root-command");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandFromMethod() {
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
registerFromMethod(commandCatalog);
|
||||
BashCompletions completions = new BashCompletions(context, commandCatalog);
|
||||
String bash = completions.generate("root-command");
|
||||
System.out.println(bash);
|
||||
assertThat(bash).contains("root-command");
|
||||
assertThat(bash).contains("commands+=(\"testmethod1\")");
|
||||
assertThat(bash).contains("_root-command_testmethod1()");
|
||||
assertThat(bash).contains("two_word_flags+=(\"--arg1\")");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandFromFunction() {
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
registerFromFunction(commandCatalog, "testmethod1");
|
||||
BashCompletions completions = new BashCompletions(context, commandCatalog);
|
||||
String bash = completions.generate("root-command");
|
||||
assertThat(bash).contains("root-command");
|
||||
assertThat(bash).contains("commands+=(\"testmethod1\")");
|
||||
assertThat(bash).contains("_root-command_testmethod1()");
|
||||
assertThat(bash).contains("two_word_flags+=(\"--arg1\")");
|
||||
}
|
||||
|
||||
private void registerFromMethod(CommandCatalog commandCatalog) {
|
||||
Pojo1 pojo1 = new Pojo1();
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("testmethod1")
|
||||
.withTarget()
|
||||
.method(pojo1, "method1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.build();
|
||||
commandCatalog.register(registration);
|
||||
}
|
||||
|
||||
private void registerFromFunction(CommandCatalog commandCatalog, String command) {
|
||||
Function<CommandContext, String> function = ctx -> {
|
||||
String arg1 = ctx.getOptionValue("arg1");
|
||||
return String.format("hi, arg1 value is '%s'", arg1);
|
||||
};
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command(command)
|
||||
.withTarget()
|
||||
.function(function)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.build();
|
||||
commandCatalog.register(registration);
|
||||
}
|
||||
|
||||
protected static class Pojo1 {
|
||||
|
||||
void method1() {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user