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,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementing this interface allows sub-systems (such as the {@literal help} command) to
|
||||
* discover available commands.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandRegistry {
|
||||
|
||||
/**
|
||||
* Return the mapping from command trigger keywords to implementation.
|
||||
*/
|
||||
Map<String, MethodTarget> listCommands();
|
||||
|
||||
/**
|
||||
* Register a new command.
|
||||
*
|
||||
* @param name the command name
|
||||
* @param target the method target
|
||||
*/
|
||||
void addCommand(String name, MethodTarget target);
|
||||
|
||||
/**
|
||||
* Deregister a command.
|
||||
*
|
||||
* @param name the command name
|
||||
*/
|
||||
void removeCommand(String name);
|
||||
}
|
||||
@@ -1,79 +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;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.context.ShellContext;
|
||||
|
||||
/**
|
||||
* A {@link CommandRegistry} that supports registration of new commands.
|
||||
*
|
||||
* <p>Makes sure that no two commands are registered with the same name.</p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ConfigurableCommandRegistry implements CommandRegistry {
|
||||
|
||||
private final ShellContext shellContext;
|
||||
private Map<String, MethodTarget> commands = new HashMap<>();
|
||||
|
||||
public ConfigurableCommandRegistry(ShellContext shellContext) {
|
||||
this.shellContext = shellContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> listCommands() {
|
||||
return commands.entrySet().stream()
|
||||
.filter(e -> {
|
||||
InteractionMode mim = e.getValue().getInteractionMode();
|
||||
InteractionMode cim = shellContext.getInteractionMode();
|
||||
if (mim == null || cim == null || mim == InteractionMode.ALL) {
|
||||
return true;
|
||||
}
|
||||
else if (mim == InteractionMode.INTERACTIVE) {
|
||||
return cim == InteractionMode.INTERACTIVE || cim == InteractionMode.ALL;
|
||||
}
|
||||
else if (mim == InteractionMode.NONINTERACTIVE) {
|
||||
return cim == InteractionMode.NONINTERACTIVE || cim == InteractionMode.ALL;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCommand(String name, MethodTarget target) {
|
||||
commands.put(name, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCommand(String name) {
|
||||
commands.remove(name);
|
||||
}
|
||||
|
||||
public void register(String name, MethodTarget target) {
|
||||
MethodTarget previous = commands.get(name);
|
||||
if (previous != null) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Illegal registration for command '%s': Attempt to register both '%s' and '%s'", name, target, previous));
|
||||
}
|
||||
commands.put(name, target);
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-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;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Represents a shell command behavior, <em>i.e.</em> code to be executed when a command is requested.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class MethodTarget implements Command {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Object bean;
|
||||
|
||||
private final Help help;
|
||||
|
||||
private final InteractionMode interactionMode;
|
||||
|
||||
/**
|
||||
* If not null, returns whether or not the command is currently available. Implementations must be idempotent.
|
||||
*/
|
||||
private final Supplier<Availability> availabilityIndicator;
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help) {
|
||||
this(method, bean, new Help(help, null), null);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help, Supplier<Availability> availabilityIndicator) {
|
||||
this(method, bean, new Help(help, null), availabilityIndicator);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, Help help, Supplier<Availability> availabilityIndicator) {
|
||||
this(method, bean, help, availabilityIndicator, null);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, Help help, Supplier<Availability> availabilityIndicator, InteractionMode interactionMode) {
|
||||
Assert.notNull(method, "Method cannot be null");
|
||||
Assert.notNull(bean, "Bean cannot be null");
|
||||
Assert.hasText(help.getDescription(), String.format("Help cannot be blank when trying to define command based on '%s'", method));
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
this.method = method;
|
||||
this.bean = bean;
|
||||
this.help = help;
|
||||
this.availabilityIndicator = availabilityIndicator != null ? availabilityIndicator : () -> Availability.available();
|
||||
this.interactionMode = interactionMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, String description, String group) {
|
||||
return of(name, bean, new Help(description, group));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, Help help) {
|
||||
return of(name, bean, help, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, Help help, Supplier<Availability> availabilityIndicator) {
|
||||
Set<Method> found = new HashSet<>();
|
||||
ReflectionUtils.doWithMethods(bean.getClass(), found::add, m -> m.getName().equals(name));
|
||||
if (found.size() != 1) {
|
||||
throw new IllegalArgumentException(String.format("Could not find unique method named '%s' on object of class %s. Found %s",
|
||||
name, bean.getClass(), found));
|
||||
}
|
||||
return new MethodTarget(found.iterator().next(), bean, help, availabilityIndicator);
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public Object getBean() {
|
||||
return bean;
|
||||
}
|
||||
|
||||
public String getHelp() {
|
||||
return help.getDescription();
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return help.getGroup();
|
||||
}
|
||||
|
||||
public Availability getAvailability() {
|
||||
return availabilityIndicator.get();
|
||||
}
|
||||
|
||||
public InteractionMode getInteractionMode() {
|
||||
return interactionMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
MethodTarget that = (MethodTarget) o;
|
||||
|
||||
if (!method.equals(that.method)) return false;
|
||||
if (!bean.equals(that.bean)) return false;
|
||||
if (!help.equals(that.help)) return false;
|
||||
return help.equals(that.help);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = method.hashCode();
|
||||
result = 31 * result + bean.hashCode();
|
||||
result = 31 * result + help.hashCode();
|
||||
result = 31 * result + help.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return method.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2017 the original author or authors.
|
||||
* Copyright 2015-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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
|
||||
/**
|
||||
* Strategy interface for registering commands.
|
||||
*
|
||||
@@ -27,6 +29,6 @@ public interface MethodTargetRegistrar {
|
||||
/**
|
||||
* Register mappings from {@literal <command keyword(s)>} to actual behavior.
|
||||
*/
|
||||
void register(ConfigurableCommandRegistry registry);
|
||||
void register(CommandCatalog registry);
|
||||
|
||||
}
|
||||
|
||||
@@ -34,15 +34,10 @@ import javax.validation.metadata.ElementDescriptor;
|
||||
*/
|
||||
public class ParameterDescription {
|
||||
|
||||
/**
|
||||
* The original method parameter this is describing.
|
||||
*/
|
||||
private final MethodParameter parameter;
|
||||
|
||||
/**
|
||||
* A string representation of the type of the parameter.
|
||||
*/
|
||||
private final String type;
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* A string representation of the parameter, as it should appear in a parameter list.
|
||||
@@ -85,15 +80,8 @@ public class ParameterDescription {
|
||||
*/
|
||||
private ElementDescriptor elementDescriptor;
|
||||
|
||||
public ParameterDescription(MethodParameter parameter, String type) {
|
||||
this.parameter = parameter;
|
||||
public void type(String type) {
|
||||
this.type = type;
|
||||
this.formal = type;
|
||||
}
|
||||
|
||||
public static ParameterDescription outOf(MethodParameter parameter) {
|
||||
Class<?> type = parameter.getParameterType();
|
||||
return new ParameterDescription(parameter, Utils.unCamelify(type.getSimpleName()));
|
||||
}
|
||||
|
||||
public ParameterDescription help(String help) {
|
||||
@@ -171,17 +159,13 @@ public class ParameterDescription {
|
||||
return String.format("%s %s", keys.isEmpty() ? "" : keys().iterator().next(), formal());
|
||||
}
|
||||
|
||||
public MethodParameter parameter() {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ParameterDescription that = (ParameterDescription) o;
|
||||
return mandatoryKey == that.mandatoryKey &&
|
||||
Objects.equals(parameter, that.parameter) &&
|
||||
// Objects.equals(parameter, that.parameter) &&
|
||||
Objects.equals(type, that.type) &&
|
||||
Objects.equals(formal, that.formal) &&
|
||||
Objects.equals(defaultValue, that.defaultValue) &&
|
||||
@@ -192,6 +176,6 @@ public class ParameterDescription {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(parameter, type, formal, defaultValue, defaultValueWhenFlag, keys, mandatoryKey, help);
|
||||
return Objects.hash(type, formal, defaultValue, defaultValueWhenFlag, keys, mandatoryKey, help);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2017 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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible, once the command has been identified, of transforming the textual
|
||||
* input to an actual parameter object.
|
||||
*
|
||||
* <p>
|
||||
* An order can also be specified in case more than one {@link ParameterResolver} supports a {@link MethodParameter}.
|
||||
* See {@link AnnotationAwareOrderComparator} for details..
|
||||
* </p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Camilo Gonzalez
|
||||
*/
|
||||
public interface ParameterResolver {
|
||||
|
||||
/**
|
||||
* Should return true if this resolver recognizes the given method parameter (<em>e.g.</em> it
|
||||
* has the correct annotation or the correct type).
|
||||
*/
|
||||
boolean supports(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Turn the given textual input into an actual object, maybe using some conversion or lookup mechanism.
|
||||
*/
|
||||
ValueResult resolve(MethodParameter methodParameter, List<String> words);
|
||||
|
||||
/**
|
||||
* Describe a supported parameter, so that integrated help can be generated.
|
||||
* <p>Typical implementations will return a one element stream result, but some may return several (for
|
||||
* example if binding several words to a POJO).</p>
|
||||
*/
|
||||
Stream<ParameterDescription> describe(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Invoked during TAB completion. If the {@link CompletionContext} can be interpreted as the start
|
||||
* of a supported {@link MethodParameter} value, one or several proposals should be returned.
|
||||
*/
|
||||
List<CompletionProposal> complete(MethodParameter parameter, CompletionContext context);
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
@@ -23,21 +22,17 @@ import java.util.Set;
|
||||
* Thrown when one or more parameters fail bean validation constraints.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ParameterValidationException extends RuntimeException {
|
||||
private final Set<ConstraintViolation<Object>> constraintViolations;
|
||||
private final MethodTarget methodTarget;
|
||||
|
||||
public ParameterValidationException(Set<ConstraintViolation<Object>> constraintViolations, MethodTarget methodTarget) {
|
||||
private final Set<ConstraintViolation<Object>> constraintViolations;
|
||||
|
||||
public ParameterValidationException(Set<ConstraintViolation<Object>> constraintViolations) {
|
||||
this.constraintViolations = constraintViolations;
|
||||
this.methodTarget = methodTarget;
|
||||
}
|
||||
|
||||
public Set<ConstraintViolation<Object>> getConstraintViolations() {
|
||||
return constraintViolations;
|
||||
}
|
||||
|
||||
public MethodTarget getMethodTarget() {
|
||||
return methodTarget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -13,45 +13,36 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.ValidatorFactory;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.Signals;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandExecution;
|
||||
import org.springframework.shell.command.CommandExecution.CommandExecutionException;
|
||||
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
|
||||
/**
|
||||
* Main class implementing a shell loop.
|
||||
*
|
||||
* <p>
|
||||
* Given some textual input, locate the {@link MethodTarget} to invoke and
|
||||
* {@link ResultHandler#handleResult(Object) handle} the result.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Also provides hooks for code completion
|
||||
* </p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@@ -66,11 +57,10 @@ public class Shell {
|
||||
*/
|
||||
public static final Object NO_INPUT = new Object();
|
||||
|
||||
private final CommandRegistry commandRegistry;
|
||||
|
||||
private Validator validator = Utils.defaultValidator();
|
||||
|
||||
protected List<ParameterResolver> parameterResolvers;
|
||||
private final Terminal terminal;
|
||||
private final CommandCatalog commandRegistry;
|
||||
protected List<CompletionResolver> completionResolvers = new ArrayList<>();
|
||||
private CommandExecutionHandlerMethodArgumentResolvers argumentResolvers;
|
||||
|
||||
/**
|
||||
* Marker object to distinguish unresolved arguments from {@code null}, which is a valid
|
||||
@@ -78,9 +68,23 @@ public class Shell {
|
||||
*/
|
||||
protected static final Object UNRESOLVED = new Object();
|
||||
|
||||
public Shell(ResultHandlerService resultHandlerService, CommandRegistry commandRegistry) {
|
||||
private Validator validator = Utils.defaultValidator();
|
||||
|
||||
public Shell(ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, Terminal terminal) {
|
||||
this.resultHandlerService = resultHandlerService;
|
||||
this.commandRegistry = commandRegistry;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setCompletionResolvers(List<CompletionResolver> resolvers) {
|
||||
this.completionResolvers = new ArrayList<>(resolvers);
|
||||
AnnotationAwareOrderComparator.sort(completionResolvers);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setArgumentResolvers(CommandExecutionHandlerMethodArgumentResolvers argumentResolvers) {
|
||||
this.argumentResolvers = argumentResolvers;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
@@ -88,12 +92,6 @@ public class Shell {
|
||||
this.validator = validatorFactory.getValidator();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setParameterResolvers(List<ParameterResolver> resolvers) {
|
||||
this.parameterResolvers = new ArrayList<>(resolvers);
|
||||
AnnotationAwareOrderComparator.sort(parameterResolvers);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main program loop: acquire input, try to match it to a command and evaluate. Repeat
|
||||
* until a {@link ResultHandler} causes the process to exit or there is no input.
|
||||
@@ -145,21 +143,24 @@ public class Shell {
|
||||
String command = findLongestCommand(line);
|
||||
|
||||
List<String> words = input.words();
|
||||
log.debug("Evaluate input with line=[{}], command=[{}]", line, command);
|
||||
if (command != null) {
|
||||
Map<String, MethodTarget> methodTargets = commandRegistry.listCommands();
|
||||
MethodTarget methodTarget = methodTargets.get(command);
|
||||
Availability availability = methodTarget.getAvailability();
|
||||
if (availability.isAvailable()) {
|
||||
|
||||
Optional<CommandRegistration> commandRegistration = commandRegistry.getRegistrations().values().stream()
|
||||
.filter(r -> {
|
||||
return r.getCommand().equals(command);
|
||||
})
|
||||
.findFirst();
|
||||
|
||||
if (commandRegistration.isPresent()) {
|
||||
List<String> wordsForArgs = wordsForArguments(command, words);
|
||||
Method method = methodTarget.getMethod();
|
||||
|
||||
Thread commandThread = Thread.currentThread();
|
||||
Object sh = Signals.register("INT", () -> commandThread.interrupt());
|
||||
try {
|
||||
Object[] args = resolveArgs(method, wordsForArgs);
|
||||
validateArgs(args, methodTarget);
|
||||
|
||||
return ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
||||
CommandExecution execution = CommandExecution
|
||||
.of(argumentResolvers != null ? argumentResolvers.getResolvers() : null, validator, terminal);
|
||||
return execution.evaluate(commandRegistration.get(), wordsForArgs.toArray(new String[0]));
|
||||
}
|
||||
catch (UndeclaredThrowableException e) {
|
||||
if (e.getCause() instanceof InterruptedException || e.getCause() instanceof ClosedByInterruptException) {
|
||||
@@ -167,6 +168,9 @@ public class Shell {
|
||||
}
|
||||
return e.getCause();
|
||||
}
|
||||
catch (CommandExecutionException e) {
|
||||
return e.getCause();
|
||||
}
|
||||
catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
@@ -175,7 +179,7 @@ public class Shell {
|
||||
}
|
||||
}
|
||||
else {
|
||||
return new CommandNotCurrentlyAvailable(command, availability);
|
||||
return new CommandNotFound(words);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -183,6 +187,7 @@ public class Shell {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return true if the parsed input ends up being empty (<em>e.g.</em> hitting ENTER on an
|
||||
* empty line or blank space).
|
||||
@@ -229,18 +234,11 @@ public class Shell {
|
||||
if (best != null) {
|
||||
CompletionContext argsContext = context.drop(best.split(" ").length);
|
||||
// Try to complete arguments
|
||||
Map<String, MethodTarget> methodTargets = commandRegistry.listCommands();
|
||||
MethodTarget methodTarget = methodTargets.get(best);
|
||||
Method method = methodTarget.getMethod();
|
||||
CommandRegistration registration = commandRegistry.getRegistrations().get(best);
|
||||
|
||||
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
|
||||
for (ParameterResolver resolver : parameterResolvers) {
|
||||
for (int index = 0; index < parameters.size(); index++) {
|
||||
MethodParameter parameter = parameters.get(index);
|
||||
if (resolver.supports(parameter)) {
|
||||
resolver.complete(parameter, argsContext).stream().forEach(candidates::add);
|
||||
}
|
||||
}
|
||||
for (CompletionResolver resolver : completionResolvers) {
|
||||
List<CompletionProposal> resolved = resolver.resolve(registration, argsContext);
|
||||
candidates.addAll(resolved);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
@@ -250,61 +248,23 @@ public class Shell {
|
||||
// Workaround for https://github.com/spring-projects/spring-shell/issues/150
|
||||
// (sadly, this ties this class to JLine somehow)
|
||||
int lastWordStart = prefix.lastIndexOf(' ') + 1;
|
||||
Map<String, MethodTarget> methodTargets = commandRegistry.listCommands();
|
||||
return methodTargets.entrySet().stream()
|
||||
.filter(e -> e.getKey().startsWith(prefix))
|
||||
.map(e -> toCommandProposal(e.getKey().substring(lastWordStart), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
return commandRegistry.getRegistrations().values().stream()
|
||||
.filter(r -> {
|
||||
return r.getCommand().startsWith(prefix);
|
||||
})
|
||||
.map(r -> {
|
||||
String c = r.getCommand();
|
||||
c = c.substring(lastWordStart);
|
||||
return toCommandProposal(c, r);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CompletionProposal toCommandProposal(String command, MethodTarget methodTarget) {
|
||||
private CompletionProposal toCommandProposal(String command, CommandRegistration registration) {
|
||||
return new CompletionProposal(command)
|
||||
.dontQuote(true)
|
||||
.category("Available commands")
|
||||
.description(methodTarget.getHelp());
|
||||
}
|
||||
|
||||
private void validateArgs(Object[] args, MethodTarget methodTarget) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] == UNRESOLVED) {
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(methodTarget.getMethod(), i);
|
||||
throw new IllegalStateException("Could not resolve " + methodParameter);
|
||||
}
|
||||
}
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables().validateParameters(
|
||||
methodTarget.getBean(),
|
||||
methodTarget.getMethod(),
|
||||
args);
|
||||
if (constraintViolations.size() > 0) {
|
||||
throw new ParameterValidationException(constraintViolations, methodTarget);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use all known {@link ParameterResolver}s to try to compute a value for each parameter
|
||||
* of the method to invoke.
|
||||
* @param method the method for which parameters should be computed
|
||||
* @param wordsForArgs the list of 'words' that should be converted to parameter values.
|
||||
* May include markers for passing parameters 'by name'
|
||||
* @return an array containing resolved parameter values, or {@link #UNRESOLVED} for
|
||||
* parameters that could not be resolved
|
||||
*/
|
||||
private Object[] resolveArgs(Method method, List<String> wordsForArgs) {
|
||||
log.debug("Resolving args {} {}", method, wordsForArgs);
|
||||
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
|
||||
Object[] args = new Object[parameters.size()];
|
||||
Arrays.fill(args, UNRESOLVED);
|
||||
for (ParameterResolver resolver : parameterResolvers) {
|
||||
log.debug("Resolving args with {}", resolver);
|
||||
for (int argIndex = 0; argIndex < args.length; argIndex++) {
|
||||
MethodParameter parameter = parameters.get(argIndex);
|
||||
if (args[argIndex] == UNRESOLVED && resolver.supports(parameter)) {
|
||||
args[argIndex] = resolver.resolve(parameter, wordsForArgs).resolvedValue();
|
||||
log.debug("Resolved {} {} {} {}", method, args[argIndex], resolver, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
return args;
|
||||
.description(registration.getHelp());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,8 +273,7 @@ public class Shell {
|
||||
* @return a valid command name, or {@literal null} if none matched
|
||||
*/
|
||||
private String findLongestCommand(String prefix) {
|
||||
Map<String, MethodTarget> methodTargets = commandRegistry.listCommands();
|
||||
String result = methodTargets.keySet().stream()
|
||||
String result = commandRegistry.getRegistrations().keySet().stream()
|
||||
.filter(command -> prefix.equals(command) || prefix.startsWith(command + " "))
|
||||
.reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2);
|
||||
return "".equals(result) ? null : result;
|
||||
|
||||
@@ -20,7 +20,10 @@ import java.lang.reflect.Constructor;
|
||||
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.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
@@ -130,4 +133,37 @@ public class Utils {
|
||||
public static Validator defaultValidator() {
|
||||
return DEFAULT_VALIDATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split array into list of lists by predicate
|
||||
*
|
||||
* @param array the array
|
||||
* @param predicate the predicate
|
||||
* @return the list of lists
|
||||
*/
|
||||
public static <T> List<List<T>> split(T[] array, Predicate<T> predicate) {
|
||||
List<T> list = Arrays.asList(array);
|
||||
boolean[] boundaries = new boolean[array.length];
|
||||
List<List<T>> split = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
boundaries[i] = predicate.test(array[i]);
|
||||
}
|
||||
|
||||
int tail = 0;
|
||||
for (int i = 0; i < boundaries.length; i++) {
|
||||
if (boundaries[i]) {
|
||||
if (tail < i) {
|
||||
split.add(list.subList(tail, i));
|
||||
}
|
||||
tail = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (tail < array.length) {
|
||||
split.add(list.subList(tail, array.length));
|
||||
}
|
||||
|
||||
return split;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
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.messaging.handler.annotation.Header;
|
||||
import org.springframework.shell.support.AbstractArgumentMethodArgumentResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Resolver for {@link Header @Header} arguments.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ArgumentHeaderMethodArgumentResolver extends AbstractArgumentMethodArgumentResolver {
|
||||
|
||||
public ArgumentHeaderMethodArgumentResolver(ConversionService conversionService,
|
||||
@Nullable ConfigurableBeanFactory beanFactory) {
|
||||
super(conversionService, beanFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(Header.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
|
||||
Header annot = parameter.getParameterAnnotation(Header.class);
|
||||
Assert.state(annot != null, "No Header annotation");
|
||||
return new HeaderNamedValueInfo(annot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, List<String> names)
|
||||
throws Exception {
|
||||
if (names.size() == 1) {
|
||||
return message.getHeaders().get(ARGUMENT_PREFIX + names.get(0));
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMissingValue(List<String> headerName, MethodParameter parameter, Message<?> message) {
|
||||
throw new MessageHandlingException(message, "Missing header '" + headerName +
|
||||
"' for method parameter type [" + parameter.getParameterType() + "]");
|
||||
}
|
||||
|
||||
private static final class HeaderNamedValueInfo extends NamedValueInfo {
|
||||
|
||||
private HeaderNamedValueInfo(Header annotation) {
|
||||
super(Arrays.asList(annotation.name()), annotation.required(), annotation.defaultValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.context.ShellContext;
|
||||
|
||||
/**
|
||||
* Interface defining contract to handle existing {@link CommandRegistration}s.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandCatalog {
|
||||
|
||||
/**
|
||||
* Register a {@link CommandRegistration}.
|
||||
*
|
||||
* @param registration the command registration
|
||||
*/
|
||||
void register(CommandRegistration... registration);
|
||||
|
||||
/**
|
||||
* Unregister a {@link CommandRegistration}.
|
||||
*
|
||||
* @param registration the command registration
|
||||
*/
|
||||
void unregister(CommandRegistration... registration);
|
||||
|
||||
/**
|
||||
* Unregister a {@link CommandRegistration} by its command name.
|
||||
*
|
||||
* @param commandName the command name
|
||||
*/
|
||||
void unregister(String... commandName);
|
||||
|
||||
/**
|
||||
* Gets all {@link CommandRegistration}s mapped with their names.
|
||||
* Returned map is a copy and cannot be used to register new commands.
|
||||
*
|
||||
* @return all command registrations
|
||||
*/
|
||||
Map<String, CommandRegistration> getRegistrations();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandCatalog}.
|
||||
*
|
||||
* @return default command catalog
|
||||
*/
|
||||
static CommandCatalog of() {
|
||||
return new DefaultCommandCatalog(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandCatalog}.
|
||||
*
|
||||
* @param resolvers the command resolvers
|
||||
* @param shellContext the shell context
|
||||
* @return default command catalog
|
||||
*/
|
||||
static CommandCatalog of(Collection<CommandResolver> resolvers, ShellContext shellContext) {
|
||||
return new DefaultCommandCatalog(resolvers, shellContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandCatalog}.
|
||||
*/
|
||||
static class DefaultCommandCatalog implements CommandCatalog {
|
||||
|
||||
private final Map<String, CommandRegistration> commandRegistrations = new HashMap<>();
|
||||
private final Collection<CommandResolver> resolvers = new ArrayList<>();
|
||||
private final ShellContext shellContext;
|
||||
|
||||
DefaultCommandCatalog(Collection<CommandResolver> resolvers, ShellContext shellContext) {
|
||||
this.shellContext = shellContext;
|
||||
if (resolvers != null) {
|
||||
this.resolvers.addAll(resolvers);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(CommandRegistration... registration) {
|
||||
for (CommandRegistration r : registration) {
|
||||
String commandName = r.getCommand();
|
||||
commandRegistrations.put(commandName, r);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(CommandRegistration... registration) {
|
||||
for (CommandRegistration r : registration) {
|
||||
String commandName = r.getCommand();
|
||||
commandRegistrations.remove(commandName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(String... commandName) {
|
||||
for (String n : commandName) {
|
||||
commandRegistrations.remove(n);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, CommandRegistration> getRegistrations() {
|
||||
Map<String, CommandRegistration> regs = new HashMap<>();
|
||||
regs.putAll(commandRegistrations);
|
||||
for (CommandResolver resolver : resolvers) {
|
||||
resolver.resolve().stream().forEach(r -> {
|
||||
regs.put(r.getCommand(), r);
|
||||
});
|
||||
}
|
||||
return regs.entrySet().stream()
|
||||
.filter(filterByInteractionMode(shellContext))
|
||||
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter registration entries by currently set mode. Having it set to ALL or null
|
||||
* effectively disables filtering as as we only care if mode is set to interactive
|
||||
* or non-interactive.
|
||||
*/
|
||||
private static Predicate<Entry<String, CommandRegistration>> filterByInteractionMode(ShellContext shellContext) {
|
||||
return e -> {
|
||||
InteractionMode mim = e.getValue().getInteractionMode();
|
||||
InteractionMode cim = shellContext != null ? shellContext.getInteractionMode() : InteractionMode.ALL;
|
||||
if (mim == null || cim == null || mim == InteractionMode.ALL) {
|
||||
return true;
|
||||
}
|
||||
else if (mim == InteractionMode.INTERACTIVE) {
|
||||
return cim == InteractionMode.INTERACTIVE || cim == InteractionMode.ALL;
|
||||
}
|
||||
else if (mim == InteractionMode.NONINTERACTIVE) {
|
||||
return cim == InteractionMode.NONINTERACTIVE || cim == InteractionMode.ALL;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// private static String commandName(String[] commands) {
|
||||
// return Arrays.asList(commands).stream()
|
||||
// .flatMap(c -> Stream.of(c.split(" ")))
|
||||
// .filter(c -> StringUtils.hasText(c))
|
||||
// .map(c -> c.trim())
|
||||
// .collect(Collectors.joining(" "));
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
/**
|
||||
* Interface to customize a {@link CommandCatalog}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandCatalogCustomizer {
|
||||
|
||||
/**
|
||||
* Customize a command catalog.
|
||||
*
|
||||
* @param commandCatalog a command catalog
|
||||
*/
|
||||
void customize(CommandCatalog commandCatalog);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResult;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResults;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Interface containing information about current command execution.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandContext {
|
||||
|
||||
/**
|
||||
* Gets a raw args passed into a currently executing command.
|
||||
*
|
||||
* @return raw command arguments
|
||||
*/
|
||||
String[] getRawArgs();
|
||||
|
||||
/**
|
||||
* Gets if option has been mapped.
|
||||
*
|
||||
* @param name the option name
|
||||
* @return true if option has been mapped, false otherwise
|
||||
*/
|
||||
boolean hasMappedOption(String name);
|
||||
|
||||
/**
|
||||
* Gets a command option parser results.
|
||||
*
|
||||
* @return the command option parser results
|
||||
*/
|
||||
CommandParserResults getParserResults();
|
||||
|
||||
/**
|
||||
* Gets an mapped option value.
|
||||
*
|
||||
* @param <T> the type to map to
|
||||
* @param name the option name
|
||||
* @return mapped value
|
||||
*/
|
||||
<T> T getOptionValue(String name);
|
||||
|
||||
/**
|
||||
* Gets a terminal.
|
||||
*
|
||||
* @return a terminal
|
||||
*/
|
||||
Terminal getTerminal();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandContext}.
|
||||
*
|
||||
* @param args the arguments
|
||||
* @param results the results
|
||||
* @param terminal the terminal
|
||||
* @return a command context
|
||||
*/
|
||||
static CommandContext of(String[] args, CommandParserResults results, Terminal terminal) {
|
||||
return new DefaultCommandContext(args, results, terminal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandContext}.
|
||||
*/
|
||||
static class DefaultCommandContext implements CommandContext {
|
||||
|
||||
private String[] args;
|
||||
private CommandParserResults results;
|
||||
private Terminal terminal;
|
||||
|
||||
DefaultCommandContext(String[] args, CommandParserResults results, Terminal terminal) {
|
||||
this.args = args;
|
||||
this.results = results;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getRawArgs() {
|
||||
return args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMappedOption(String name) {
|
||||
return find(name).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandParserResults getParserResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getOptionValue(String name) {
|
||||
Optional<CommandParserResult> find = find(name);
|
||||
if (find.isPresent()) {
|
||||
return (T) find.get().value();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Terminal getTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
private Optional<CommandParserResult> find(String name) {
|
||||
return results.results().stream()
|
||||
.filter(r -> {
|
||||
Stream<String> l = Arrays.asList(r.option().getLongNames()).stream();
|
||||
Stream<String> s = Arrays.asList(r.option().getShortNames()).stream().map(n -> Character.toString(n));
|
||||
return Stream.concat(l, s)
|
||||
.filter(o -> ObjectUtils.nullSafeEquals(o, name))
|
||||
.findFirst()
|
||||
.isPresent();
|
||||
})
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
|
||||
/**
|
||||
* Implementation of a {@link HandlerMethodArgumentResolver} resolving
|
||||
* {@link CommandContext}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class CommandContextMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
public static final String HEADER_COMMAND_CONTEXT = "springShellCommandContext";
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
MethodParameter nestedParameter = parameter.nestedIfOptional();
|
||||
Class<?> paramType = nestedParameter.getNestedParameterType();
|
||||
return CommandContext.class.isAssignableFrom(paramType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message){
|
||||
CommandContext commandContext = message.getHeaders().get(HEADER_COMMAND_CONTEXT, CommandContext.class);
|
||||
return parameter.isOptional() ? Optional.ofNullable(commandContext) : commandContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.validation.Validator;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserException;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResults;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo.TargetType;
|
||||
import org.springframework.shell.command.invocation.InvocableShellMethod;
|
||||
import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite;
|
||||
|
||||
/**
|
||||
* Interface to evaluate a result from a command with an arguments.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandExecution {
|
||||
|
||||
/**
|
||||
* Evaluate a command with a given arguments.
|
||||
*
|
||||
* @param registration the command registration
|
||||
* @param args the command args
|
||||
* @return evaluated execution
|
||||
*/
|
||||
Object evaluate(CommandRegistration registration, String[] args);
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandExecution}.
|
||||
*
|
||||
* @param resolvers the handler method argument resolvers
|
||||
* @return default command execution
|
||||
*/
|
||||
public static CommandExecution of(List<? extends HandlerMethodArgumentResolver> resolvers) {
|
||||
return new DefaultCommandExecution(resolvers, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandExecution}.
|
||||
*
|
||||
* @param resolvers the handler method argument resolvers
|
||||
* @param validator the validator
|
||||
* @param terminal the terminal
|
||||
* @return default command execution
|
||||
*/
|
||||
public static CommandExecution of(List<? extends HandlerMethodArgumentResolver> resolvers, Validator validator,
|
||||
Terminal terminal) {
|
||||
return new DefaultCommandExecution(resolvers, validator, terminal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandExecution}.
|
||||
*/
|
||||
static class DefaultCommandExecution implements CommandExecution {
|
||||
|
||||
private List<? extends HandlerMethodArgumentResolver> resolvers;
|
||||
private Validator validator;
|
||||
private Terminal terminal;
|
||||
|
||||
public DefaultCommandExecution(List<? extends HandlerMethodArgumentResolver> resolvers, Validator validator,
|
||||
Terminal terminal) {
|
||||
this.resolvers = resolvers;
|
||||
this.validator = validator;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
public Object evaluate(CommandRegistration registration, String[] args) {
|
||||
|
||||
List<CommandOption> options = registration.getOptions();
|
||||
CommandParser parser = CommandParser.of();
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
|
||||
if (!results.errors().isEmpty()) {
|
||||
throw new CommandParserExceptionsException("Command parser resulted errors", results.errors());
|
||||
}
|
||||
|
||||
CommandContext ctx = CommandContext.of(args, results, terminal);
|
||||
|
||||
Object res = null;
|
||||
|
||||
TargetInfo targetInfo = registration.getTarget();
|
||||
|
||||
// pick the target to execute
|
||||
if (targetInfo.getTargetType() == TargetType.FUNCTION) {
|
||||
res = targetInfo.getFunction().apply(ctx);
|
||||
}
|
||||
else if (targetInfo.getTargetType() == TargetType.CONSUMER) {
|
||||
targetInfo.getConsumer().accept(ctx);
|
||||
}
|
||||
else if (targetInfo.getTargetType() == TargetType.METHOD) {
|
||||
try {
|
||||
MessageBuilder<String[]> messageBuilder = MessageBuilder.withPayload(args);
|
||||
Map<String, Object> paramValues = new HashMap<>();
|
||||
results.results().stream().forEach(r -> {
|
||||
if (r.option().getLongNames() != null) {
|
||||
for (String n : r.option().getLongNames()) {
|
||||
messageBuilder.setHeader(ArgumentHeaderMethodArgumentResolver.ARGUMENT_PREFIX + n, r.value());
|
||||
paramValues.put(n, r.value());
|
||||
}
|
||||
}
|
||||
if (r.option().getShortNames() != null) {
|
||||
for (Character n : r.option().getShortNames()) {
|
||||
messageBuilder.setHeader(ArgumentHeaderMethodArgumentResolver.ARGUMENT_PREFIX + n.toString(), r.value());
|
||||
}
|
||||
}
|
||||
});
|
||||
messageBuilder.setHeader(CommandContextMethodArgumentResolver.HEADER_COMMAND_CONTEXT, ctx);
|
||||
|
||||
InvocableShellMethod invocableShellMethod = new InvocableShellMethod(targetInfo.getBean(), targetInfo.getMethod());
|
||||
invocableShellMethod.setValidator(validator);
|
||||
ShellMethodArgumentResolverComposite argumentResolvers = new ShellMethodArgumentResolverComposite();
|
||||
if (resolvers != null) {
|
||||
argumentResolvers.addResolvers(resolvers);
|
||||
}
|
||||
if (!paramValues.isEmpty()) {
|
||||
argumentResolvers.addResolver(new ParamNameHandlerMethodArgumentResolver(paramValues));
|
||||
}
|
||||
invocableShellMethod.setMessageMethodArgumentResolvers(argumentResolvers);
|
||||
|
||||
res = invocableShellMethod.invoke(messageBuilder.build(), (Object[])null);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new CommandExecutionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@Order(100)
|
||||
static class ParamNameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final Map<String, Object> paramValues = new HashMap<>();
|
||||
ConversionService conversionService = new DefaultConversionService();
|
||||
|
||||
ParamNameHandlerMethodArgumentResolver(Map<String, Object> paramValues) {
|
||||
this.paramValues.putAll(paramValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
String parameterName = parameter.getParameterName();
|
||||
if (parameterName == null) {
|
||||
return false;
|
||||
}
|
||||
return paramValues.containsKey(parameterName) && conversionService
|
||||
.canConvert(paramValues.get(parameterName).getClass(), parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
return conversionService.convert(paramValues.get(parameter.getParameterName()), parameter.getParameterType());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CommandExecutionException extends RuntimeException {
|
||||
|
||||
public CommandExecutionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandParserExceptionsException extends RuntimeException {
|
||||
|
||||
private final List<CommandParserException> parserExceptions;
|
||||
|
||||
public CommandParserExceptionsException(String message, List<CommandParserException> parserExceptions) {
|
||||
super(message);
|
||||
this.parserExceptions = parserExceptions;
|
||||
}
|
||||
|
||||
public List<CommandParserException> getParserExceptions() {
|
||||
return parserExceptions;
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandExecutionHandlerMethodArgumentResolvers {
|
||||
|
||||
private final List<? extends HandlerMethodArgumentResolver> resolvers;
|
||||
|
||||
public CommandExecutionHandlerMethodArgumentResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) {
|
||||
this.resolvers = resolvers;
|
||||
}
|
||||
|
||||
public List<? extends HandlerMethodArgumentResolver> getResolvers() {
|
||||
return resolvers;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Interface representing an option in a command.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandOption {
|
||||
|
||||
/**
|
||||
* Gets a long names of an option.
|
||||
*
|
||||
* @return long names of an option
|
||||
*/
|
||||
String[] getLongNames();
|
||||
|
||||
/**
|
||||
* Gets a short names of an option.
|
||||
*
|
||||
* @return short names of an option
|
||||
*/
|
||||
Character[] getShortNames();
|
||||
|
||||
/**
|
||||
* Gets a description of an option.
|
||||
*
|
||||
* @return description of an option
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Gets a {@link ResolvableType} of an option.
|
||||
*
|
||||
* @return type of an option
|
||||
*/
|
||||
ResolvableType getType();
|
||||
|
||||
/**
|
||||
* Gets a flag if option is required.
|
||||
*
|
||||
* @return the required flag
|
||||
*/
|
||||
boolean isRequired();
|
||||
|
||||
/**
|
||||
* Gets a default value of an option.
|
||||
*
|
||||
* @return the default value
|
||||
*/
|
||||
String getDefaultValue();
|
||||
|
||||
/**
|
||||
* Gets a positional value.
|
||||
*
|
||||
* @return the positional value
|
||||
*/
|
||||
int getPosition();
|
||||
|
||||
/**
|
||||
* Gets a minimum arity.
|
||||
*
|
||||
* @return the minimum arity
|
||||
*/
|
||||
int getArityMin();
|
||||
|
||||
/**
|
||||
* Gets a maximum arity.
|
||||
*
|
||||
* @return the maximum arity
|
||||
*/
|
||||
int getArityMax();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandOption}.
|
||||
*
|
||||
* @param longNames the long names
|
||||
* @param shortNames the short names
|
||||
* @param description the description
|
||||
* @return default command option
|
||||
*/
|
||||
public static CommandOption of(String[] longNames, Character[] shortNames, String description) {
|
||||
return of(longNames, shortNames, description, null, false, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandOption}.
|
||||
*
|
||||
* @param longNames the long names
|
||||
* @param shortNames the short names
|
||||
* @param description the description
|
||||
* @param type the type
|
||||
* @return default command option
|
||||
*/
|
||||
public static CommandOption of(String[] longNames, Character[] shortNames, String description,
|
||||
ResolvableType type) {
|
||||
return of(longNames, shortNames, description, type, false, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandOption}.
|
||||
*
|
||||
* @param longNames the long names
|
||||
* @param shortNames the short names
|
||||
* @param description the description
|
||||
* @param type the type
|
||||
* @param required the required flag
|
||||
* @param defaultValue the default value
|
||||
* @param position the position value
|
||||
* @param arityMin the min arity
|
||||
* @param arityMax the max arity
|
||||
* @return default command option
|
||||
*/
|
||||
public static CommandOption of(String[] longNames, Character[] shortNames, String description,
|
||||
ResolvableType type, boolean required, String defaultValue, Integer position, Integer arityMin, Integer arityMax) {
|
||||
return new DefaultCommandOption(longNames, shortNames, description, type, required, defaultValue, position,
|
||||
arityMin, arityMax);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link CommandOption}.
|
||||
*/
|
||||
public static class DefaultCommandOption implements CommandOption {
|
||||
|
||||
private String[] longNames;
|
||||
private Character[] shortNames;
|
||||
private String description;
|
||||
private ResolvableType type;
|
||||
private boolean required;
|
||||
private String defaultValue;
|
||||
private int position;
|
||||
private int arityMin;
|
||||
private int arityMax;
|
||||
|
||||
public DefaultCommandOption(String[] longNames, Character[] shortNames, String description,
|
||||
ResolvableType type, boolean required, String defaultValue, Integer position,
|
||||
Integer arityMin, Integer arityMax) {
|
||||
this.longNames = longNames != null ? longNames : new String[0];
|
||||
this.shortNames = shortNames != null ? shortNames : new Character[0];
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
this.required = required;
|
||||
this.defaultValue = defaultValue;
|
||||
this.position = position != null && position > -1 ? position : -1 ;
|
||||
this.arityMin = arityMin != null ? arityMin : -1;
|
||||
this.arityMax = arityMax != null ? arityMax : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getLongNames() {
|
||||
return longNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Character[] getShortNames() {
|
||||
return shortNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getArityMin() {
|
||||
return arityMin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getArityMax() {
|
||||
return arityMax;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Interface parsing arguments for a {@link CommandRegistration}. A command is
|
||||
* always identified by a set of words like
|
||||
* {@code command subcommand1 subcommand2} and remaining part of it are options
|
||||
* which this interface intercepts and translates into format we can understand.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandParser {
|
||||
|
||||
/**
|
||||
* Result of a parsing {@link CommandOption} with an argument.
|
||||
*/
|
||||
interface CommandParserResult {
|
||||
|
||||
/**
|
||||
* Gets the {@link CommandOption}.
|
||||
*
|
||||
* @return the command option
|
||||
*/
|
||||
CommandOption option();
|
||||
|
||||
/**
|
||||
* Gets the value.
|
||||
*
|
||||
* @return the value
|
||||
*/
|
||||
Object value();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandParserResult}.
|
||||
*
|
||||
* @param option the command option
|
||||
* @param value the value
|
||||
* @return a result
|
||||
*/
|
||||
static CommandParserResult of(CommandOption option, Object value) {
|
||||
return new DefaultCommandParserResult(option, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Results of a {@link CommandParser}. Basically contains a list of {@link CommandParserResult}s.
|
||||
*/
|
||||
interface CommandParserResults {
|
||||
|
||||
/**
|
||||
* Gets the results.
|
||||
*
|
||||
* @return the results
|
||||
*/
|
||||
List<CommandParserResult> results();
|
||||
|
||||
/**
|
||||
* Gets the unmapped positional arguments.
|
||||
*
|
||||
* @return the unmapped positional arguments
|
||||
*/
|
||||
List<String> positional();
|
||||
|
||||
/**
|
||||
* Gets parsing errors.
|
||||
*
|
||||
* @return the parsing errors
|
||||
*/
|
||||
List<CommandParserException> errors();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandParserResults}.
|
||||
*
|
||||
* @param results the results
|
||||
* @param positional the list of positional arguments
|
||||
* @param errors the parsing errors
|
||||
* @return a new instance of results
|
||||
*/
|
||||
static CommandParserResults of(List<CommandParserResult> results, List<String> positional, List<CommandParserException> errors) {
|
||||
return new DefaultCommandParserResults(results, positional, errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse options with a given arguments.
|
||||
*
|
||||
* May throw various runtime exceptions depending how parser is configure.
|
||||
* For example if required option is missing an exception is thrown.
|
||||
*
|
||||
* @param options the command options
|
||||
* @param args the arguments
|
||||
* @return parsed results
|
||||
*/
|
||||
CommandParserResults parse(List<CommandOption> options, String[] args);
|
||||
|
||||
/**
|
||||
* Gets an instance of a default command parser.
|
||||
*
|
||||
* @return instance of a default command parser
|
||||
*/
|
||||
static CommandParser of() {
|
||||
return new DefaultCommandParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandParserResults}.
|
||||
*/
|
||||
static class DefaultCommandParserResults implements CommandParserResults {
|
||||
|
||||
private List<CommandParserResult> results;
|
||||
private List<String> positional;
|
||||
private List<CommandParserException> errors;
|
||||
|
||||
DefaultCommandParserResults(List<CommandParserResult> results, List<String> positional, List<CommandParserException> errors) {
|
||||
this.results = results;
|
||||
this.positional = positional;
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandParserResult> results() {
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> positional() {
|
||||
return positional;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandParserException> errors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandParserResult}.
|
||||
*/
|
||||
static class DefaultCommandParserResult implements CommandParserResult {
|
||||
|
||||
private CommandOption option;
|
||||
private Object value;
|
||||
|
||||
DefaultCommandParserResult(CommandOption option, Object value) {
|
||||
this.option = option;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandOption option() {
|
||||
return option;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandParser}.
|
||||
*/
|
||||
static class DefaultCommandParser implements CommandParser {
|
||||
|
||||
@Override
|
||||
public CommandParserResults parse(List<CommandOption> options, String[] args) {
|
||||
List<CommandOption> requiredOptions = options.stream()
|
||||
.filter(o -> o.isRequired())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Lexer lexer = new Lexer(args);
|
||||
List<List<String>> lexerResults = lexer.visit();
|
||||
Parser parser = new Parser();
|
||||
ParserResults parserResults = parser.visit(lexerResults, options);
|
||||
|
||||
List<CommandParserResult> results = new ArrayList<>();
|
||||
List<String> positional = new ArrayList<>();
|
||||
List<CommandParserException> errors = new ArrayList<>();
|
||||
parserResults.results.stream().forEach(pr -> {
|
||||
if (pr.option != null) {
|
||||
results.add(new DefaultCommandParserResult(pr.option, pr.value));
|
||||
requiredOptions.remove(pr.option);
|
||||
}
|
||||
else {
|
||||
positional.addAll(pr.args);
|
||||
}
|
||||
if (pr.error != null) {
|
||||
errors.add(pr.error);
|
||||
}
|
||||
});
|
||||
|
||||
Deque<ParserResult> queue = new ArrayDeque<>(parserResults.results);
|
||||
options.stream()
|
||||
.filter(o -> o.getPosition() > -1)
|
||||
.sorted(Comparator.comparingInt(o -> o.getPosition()))
|
||||
.forEach(o -> {
|
||||
int arityMin = o.getArityMin();
|
||||
int arityMax = o.getArityMax();
|
||||
List<String> oargs = new ArrayList<>();
|
||||
if (arityMin > -1) {
|
||||
for (int i = 0; i < arityMax; i++) {
|
||||
ParserResult pop = null;
|
||||
if (!queue.isEmpty()) {
|
||||
pop = queue.pop();
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
if (pop != null && pop.option == null) {
|
||||
if (!pop.args.isEmpty()) {
|
||||
oargs.add(pop.args.stream().collect(Collectors.joining(" ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!oargs.isEmpty()) {
|
||||
results.add(new DefaultCommandParserResult(o, oargs.stream().collect(Collectors.joining(" "))));
|
||||
requiredOptions.remove(o);
|
||||
}
|
||||
});
|
||||
|
||||
requiredOptions.stream().forEach(o -> {
|
||||
String ln = o.getLongNames() != null ? Stream.of(o.getLongNames()).collect(Collectors.joining(",")) : "";
|
||||
String sn = o.getShortNames() != null ? Stream.of(o.getShortNames()).map(n -> Character.toString(n))
|
||||
.collect(Collectors.joining(",")) : "";
|
||||
errors.add(MissingOptionException
|
||||
.of(String.format("Missing option, longnames='%s', shortnames='%s'", ln, sn), o));
|
||||
});
|
||||
|
||||
return new DefaultCommandParserResults(results, positional, errors);
|
||||
}
|
||||
|
||||
private static class ParserResult {
|
||||
private CommandOption option;
|
||||
private List<String> args;
|
||||
private Object value;
|
||||
private CommandParserException error;
|
||||
|
||||
private ParserResult(CommandOption option, List<String> args, Object value, CommandParserException error) {
|
||||
this.option = option;
|
||||
this.args = args;
|
||||
this.value = value;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
static ParserResult of(CommandOption option, List<String> args, Object value,
|
||||
CommandParserException error) {
|
||||
return new ParserResult(option, args, value, error);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParserResults {
|
||||
private List<ParserResult> results;
|
||||
|
||||
private ParserResults(List<ParserResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
static ParserResults of(List<ParserResult> results) {
|
||||
return new ParserResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser works on a results from a lexer. It looks for given options
|
||||
* and builds parsing results.
|
||||
*/
|
||||
private static class Parser {
|
||||
ParserResults visit(List<List<String>> lexerResults, List<CommandOption> options) {
|
||||
List<ParserResult> results = lexerResults.stream()
|
||||
.flatMap(lr -> {
|
||||
List<CommandOption> option = matchOptions(options, lr.get(0));
|
||||
if (option.isEmpty()) {
|
||||
return lr.stream().map(a -> ParserResult.of(null, Arrays.asList(a), null, null));
|
||||
}
|
||||
else {
|
||||
return option.stream().flatMap(o -> {
|
||||
List<String> subArgs = lr.subList(1, lr.size());
|
||||
ConvertArgumentsHolder holder = convertArguments(o, subArgs);
|
||||
Object value = holder.value;
|
||||
Stream<ParserResult> unmapped = holder.unmapped.stream()
|
||||
.map(um -> ParserResult.of(null, Arrays.asList(um), null, null));
|
||||
Stream<ParserResult> res = Stream.of(ParserResult.of(o, subArgs, value, null));
|
||||
return Stream.concat(res, unmapped);
|
||||
});
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return ParserResults.of(results);
|
||||
}
|
||||
|
||||
private List<CommandOption> matchOptions(List<CommandOption> options, String arg) {
|
||||
List<CommandOption> matched = new ArrayList<>();
|
||||
String trimmed = StringUtils.trimLeadingCharacter(arg, '-');
|
||||
int count = arg.length() - trimmed.length();
|
||||
if (count == 1) {
|
||||
if (trimmed.length() == 1) {
|
||||
Character trimmedChar = trimmed.charAt(0);
|
||||
options.stream()
|
||||
.filter(o -> {
|
||||
for (Character sn : o.getShortNames()) {
|
||||
if (trimmedChar.equals(sn)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(o -> matched.add(o));
|
||||
}
|
||||
else if (trimmed.length() > 1) {
|
||||
trimmed.chars().mapToObj(i -> (char)i)
|
||||
.forEach(c -> {
|
||||
options.stream().forEach(o -> {
|
||||
for (Character sn : o.getShortNames()) {
|
||||
if (c.equals(sn)) {
|
||||
matched.add(o);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (count == 2) {
|
||||
options.stream()
|
||||
.filter(o -> {
|
||||
for (String ln : o.getLongNames()) {
|
||||
if (trimmed.equals(ln)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(o -> matched.add(o));
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private ConvertArgumentsHolder convertArguments(CommandOption option, List<String> arguments) {
|
||||
Object value = null;
|
||||
List<String> unmapped = new ArrayList<>();
|
||||
|
||||
ResolvableType type = option.getType();
|
||||
int arityMin = option.getArityMin();
|
||||
int arityMax = option.getArityMax();
|
||||
|
||||
if (arityMin < 0 && type != null) {
|
||||
if (type.isAssignableFrom(boolean.class)) {
|
||||
arityMin = 1;
|
||||
arityMax = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (type != null && type.isAssignableFrom(boolean.class)) {
|
||||
if (arguments.size() == 0) {
|
||||
value = true;
|
||||
}
|
||||
else {
|
||||
value = Boolean.parseBoolean(arguments.get(0));
|
||||
}
|
||||
}
|
||||
else if (type != null && type.isArray()) {
|
||||
value = arguments.stream().collect(Collectors.toList()).toArray();
|
||||
}
|
||||
else {
|
||||
if (!arguments.isEmpty()) {
|
||||
if (arguments.size() == 1) {
|
||||
value = arguments.get(0);
|
||||
}
|
||||
else {
|
||||
if (arityMax > 0) {
|
||||
int limit = Math.min(arguments.size(), arityMax);
|
||||
value = arguments.stream().limit(limit).collect(Collectors.joining(" "));
|
||||
unmapped.addAll(arguments.subList(limit, arguments.size()));
|
||||
}
|
||||
else {
|
||||
value = arguments.get(0);
|
||||
unmapped.addAll(arguments.subList(1, arguments.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConvertArgumentsHolder.of(value, unmapped);
|
||||
}
|
||||
|
||||
private static class ConvertArgumentsHolder {
|
||||
Object value;
|
||||
final List<String> unmapped = new ArrayList<>();
|
||||
|
||||
ConvertArgumentsHolder(Object value, List<String> unmapped) {
|
||||
this.value = value;
|
||||
if (unmapped != null) {
|
||||
this.unmapped.addAll(unmapped);
|
||||
}
|
||||
}
|
||||
|
||||
static ConvertArgumentsHolder of(Object value, List<String> unmapped) {
|
||||
return new ConvertArgumentsHolder(value, unmapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexers only responsibility is to splice arguments array into
|
||||
* chunks which belongs together what comes for option structure.
|
||||
*/
|
||||
private static class Lexer {
|
||||
private final String[] args;
|
||||
Lexer(String[] args) {
|
||||
this.args = args;
|
||||
}
|
||||
List<List<String>> visit() {
|
||||
return Utils.split(args, t -> t.startsWith("-"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandParserException extends RuntimeException {
|
||||
|
||||
public CommandParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CommandParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public static CommandParserException of(String message) {
|
||||
return new CommandParserException(message);
|
||||
}
|
||||
}
|
||||
|
||||
static class MissingOptionException extends CommandParserException {
|
||||
|
||||
private CommandOption option;
|
||||
|
||||
public MissingOptionException(String message, CommandOption option) {
|
||||
super(message);
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
public static MissingOptionException of(String message, CommandOption option) {
|
||||
return new MissingOptionException(message, option);
|
||||
}
|
||||
|
||||
public CommandOption getOption() {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Interface defining a command registration endpoint.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandRegistration {
|
||||
|
||||
/**
|
||||
* Gets a command for this registration.
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
String getCommand();
|
||||
|
||||
/**
|
||||
* Gets an {@link InteractionMode}.
|
||||
*
|
||||
* @return the interaction mode
|
||||
*/
|
||||
InteractionMode getInteractionMode();
|
||||
|
||||
/**
|
||||
* Get help for a command.
|
||||
*
|
||||
* @return the help
|
||||
*/
|
||||
String getHelp();
|
||||
|
||||
/**
|
||||
* Get group for a command.
|
||||
*
|
||||
* @return the group
|
||||
*/
|
||||
String getGroup();
|
||||
|
||||
/**
|
||||
* Get description for a command.
|
||||
*
|
||||
* @return the description
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Get {@link Availability} for a command
|
||||
*
|
||||
* @return the availability
|
||||
*/
|
||||
Availability getAvailability();
|
||||
|
||||
/**
|
||||
* Gets target info.
|
||||
*
|
||||
* @return the target info
|
||||
*/
|
||||
TargetInfo getTarget();
|
||||
|
||||
/**
|
||||
* Gets an options.
|
||||
*
|
||||
* @return the options
|
||||
*/
|
||||
List<CommandOption> getOptions();
|
||||
|
||||
/**
|
||||
* Gets a new instance of a {@link Buidler}.
|
||||
*
|
||||
* @return a new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new DefaultBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec defining an option.
|
||||
*/
|
||||
public interface OptionSpec {
|
||||
|
||||
/**
|
||||
* Define long option names.
|
||||
*
|
||||
* @param names the long option names
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec longNames(String... names);
|
||||
|
||||
/**
|
||||
* Define short option names.
|
||||
*
|
||||
* @param names the long option names
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec shortNames(Character... names);
|
||||
|
||||
/**
|
||||
* Define a type for an option.
|
||||
*
|
||||
* @param type the type
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec type(Type type);
|
||||
|
||||
/**
|
||||
* Define a {@code description} for an option.
|
||||
*
|
||||
* @param description the option description
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec description(String description);
|
||||
|
||||
/**
|
||||
* Define if option is required.
|
||||
*
|
||||
* @param required the required flag
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec required(boolean required);
|
||||
|
||||
/**
|
||||
* Define option to be required. Syntatic sugar calling
|
||||
* {@link #required(boolean)} with {@code true}.
|
||||
*
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec required();
|
||||
|
||||
/**
|
||||
* Define a {@code defaultValue} for an option.
|
||||
*
|
||||
* @param defaultValue the option default value
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec defaultValue(String defaultValue);
|
||||
|
||||
/**
|
||||
* Define an optional hint for possible positional mapping.
|
||||
*
|
||||
* @param position the position
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec position(Integer position);
|
||||
|
||||
/**
|
||||
* Define an {@code arity} for an option.
|
||||
*
|
||||
* @param min the min arity
|
||||
* @param max the max arity
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec arity(int min, int max);
|
||||
|
||||
/**
|
||||
* Define an {@code arity} for an option.
|
||||
*
|
||||
* @param arity the arity
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec arity(OptionArity arity);
|
||||
|
||||
/**
|
||||
* Return a builder for chaining.
|
||||
*
|
||||
* @return a builder for chaining
|
||||
*/
|
||||
Builder and();
|
||||
}
|
||||
|
||||
public enum OptionArity {
|
||||
ZERO,
|
||||
ZERO_OR_ONE,
|
||||
EXACTLY_ONE,
|
||||
ZERO_OR_MORE,
|
||||
ONE_OR_MORE
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates info for {@link TargetSpec}.
|
||||
*/
|
||||
public interface TargetInfo {
|
||||
|
||||
/**
|
||||
* Get target type
|
||||
*
|
||||
* @return the target type
|
||||
*/
|
||||
TargetType getTargetType();
|
||||
|
||||
/**
|
||||
* Get the bean.
|
||||
*
|
||||
* @return the bean
|
||||
*/
|
||||
Object getBean();
|
||||
|
||||
/**
|
||||
* Get the bean method
|
||||
*
|
||||
* @return the bean method
|
||||
*/
|
||||
Method getMethod();
|
||||
|
||||
/**
|
||||
* Get the function
|
||||
*
|
||||
* @return the function
|
||||
*/
|
||||
Function<CommandContext, ?> getFunction();
|
||||
|
||||
/**
|
||||
* Get the consumer
|
||||
*
|
||||
* @return the consumer
|
||||
*/
|
||||
Consumer<CommandContext> getConsumer();
|
||||
|
||||
static TargetInfo of(Object bean, Method method) {
|
||||
return new DefaultTargetInfo(TargetType.METHOD, bean, method, null, null);
|
||||
}
|
||||
|
||||
static TargetInfo of(Function<CommandContext, ?> function) {
|
||||
return new DefaultTargetInfo(TargetType.FUNCTION, null, null, function, null);
|
||||
}
|
||||
|
||||
static TargetInfo of(Consumer<CommandContext> consumer) {
|
||||
return new DefaultTargetInfo(TargetType.CONSUMER, null, null, null, consumer);
|
||||
}
|
||||
|
||||
enum TargetType {
|
||||
METHOD, FUNCTION, CONSUMER;
|
||||
}
|
||||
|
||||
static class DefaultTargetInfo implements TargetInfo {
|
||||
|
||||
private final TargetType targetType;
|
||||
private final Object bean;
|
||||
private final Method method;
|
||||
private final Function<CommandContext, ?> function;
|
||||
private final Consumer<CommandContext> consumer;
|
||||
|
||||
public DefaultTargetInfo(TargetType targetType, Object bean, Method method,
|
||||
Function<CommandContext, ?> function, Consumer<CommandContext> consumer) {
|
||||
this.targetType = targetType;
|
||||
this.bean = bean;
|
||||
this.method = method;
|
||||
this.function = function;
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetType getTargetType() {
|
||||
return targetType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<CommandContext, ?> getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consumer<CommandContext> getConsumer() {
|
||||
return consumer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec defining a target.
|
||||
*/
|
||||
public interface TargetSpec {
|
||||
|
||||
/**
|
||||
* Register a method target.
|
||||
*
|
||||
* @param bean the bean
|
||||
* @param method the method
|
||||
* @param paramTypes the parameter types
|
||||
* @return a target spec for chaining
|
||||
*/
|
||||
TargetSpec method(Object bean, String method, @Nullable Class<?>... paramTypes);
|
||||
|
||||
/**
|
||||
* Register a method target.
|
||||
*
|
||||
* @param bean the bean
|
||||
* @param method the method
|
||||
* @return a target spec for chaining
|
||||
*/
|
||||
TargetSpec method(Object bean, Method method);
|
||||
|
||||
/**
|
||||
* Register a function target.
|
||||
*
|
||||
* @param function the function to register
|
||||
* @return a target spec for chaining
|
||||
*/
|
||||
TargetSpec function(Function<CommandContext, ?> function);
|
||||
|
||||
/**
|
||||
* Register a consumer target.
|
||||
*
|
||||
* @param consumer the consumer to register
|
||||
* @return a target spec for chaining
|
||||
*/
|
||||
TargetSpec consumer(Consumer<CommandContext> consumer);
|
||||
|
||||
/**
|
||||
* Return a builder for chaining.
|
||||
*
|
||||
* @return a builder for chaining
|
||||
*/
|
||||
Builder and();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder interface for {@link CommandRegistration}.
|
||||
*/
|
||||
public interface Builder {
|
||||
|
||||
/**
|
||||
* Define commands this registration uses. Essentially defines a full set of
|
||||
* main and sub commands. It doesn't matter if full command is defined in one
|
||||
* string or multiple strings as "words" are splitted and trimmed with
|
||||
* whitespaces. You will get result of {@code command subcommand1 subcommand2, ...}.
|
||||
*
|
||||
* @param commands the commands
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder command(String... commands);
|
||||
|
||||
/**
|
||||
* Define {@link InteractionMode} for a command.
|
||||
*
|
||||
* @param mode the interaction mode
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder interactionMode(InteractionMode mode);
|
||||
|
||||
/**
|
||||
* Define a simple help text for a command.
|
||||
*
|
||||
* @param help the help text
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder help(String help);
|
||||
|
||||
/**
|
||||
* Define an {@link Availability} suppliear for a command.
|
||||
*
|
||||
* @param availability the availability
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder availability(Supplier<Availability> availability);
|
||||
|
||||
/**
|
||||
* Define a group for a command.
|
||||
*
|
||||
* @param group the group
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder group(String group);
|
||||
|
||||
/**
|
||||
* Define an option what this command should user for. Can be used multiple
|
||||
* times.
|
||||
*
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec withOption();
|
||||
|
||||
/**
|
||||
* Define a target what this command should execute
|
||||
*
|
||||
* @return target spec for chaining
|
||||
*/
|
||||
TargetSpec withTarget();
|
||||
|
||||
/**
|
||||
* Builds a {@link CommandRegistration}.
|
||||
*
|
||||
* @return a command registration
|
||||
*/
|
||||
CommandRegistration build();
|
||||
}
|
||||
|
||||
static class DefaultOptionSpec implements OptionSpec {
|
||||
|
||||
private BaseBuilder builder;
|
||||
private String[] longNames;
|
||||
private Character[] shortNames;
|
||||
private ResolvableType type;
|
||||
private String description;
|
||||
private boolean required;
|
||||
private String defaultValue;
|
||||
private Integer position;
|
||||
private Integer arityMin;
|
||||
private Integer arityMax;
|
||||
|
||||
DefaultOptionSpec(BaseBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec longNames(String... names) {
|
||||
this.longNames = names;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec shortNames(Character... names) {
|
||||
this.shortNames = names;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec type(Type type) {
|
||||
this.type = ResolvableType.forType(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec required(boolean required) {
|
||||
this.required = required;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec required() {
|
||||
return required(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec defaultValue(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec position(Integer position) {
|
||||
this.position = position;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec arity(int min, int max) {
|
||||
Assert.isTrue(min > -1, "arity min must be 0 or more");
|
||||
Assert.isTrue(max >= min, "arity max must be equal more than min");
|
||||
this.arityMin = min;
|
||||
this.arityMax = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec arity(OptionArity arity) {
|
||||
switch (arity) {
|
||||
case ZERO:
|
||||
this.arityMin = 0;
|
||||
this.arityMax = 0;
|
||||
break;
|
||||
case ZERO_OR_ONE:
|
||||
this.arityMin = 0;
|
||||
this.arityMax = Integer.MAX_VALUE;
|
||||
break;
|
||||
case EXACTLY_ONE:
|
||||
this.arityMin = 1;
|
||||
this.arityMax = 1;
|
||||
break;
|
||||
case ZERO_OR_MORE:
|
||||
this.arityMin = 0;
|
||||
this.arityMax = Integer.MAX_VALUE;
|
||||
break;
|
||||
case ONE_OR_MORE:
|
||||
this.arityMin = 1;
|
||||
this.arityMax = Integer.MAX_VALUE;
|
||||
break;
|
||||
default:
|
||||
this.arityMin = 0;
|
||||
this.arityMax = 0;
|
||||
break;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder and() {
|
||||
return builder;
|
||||
}
|
||||
|
||||
public String[] getLongNames() {
|
||||
return longNames;
|
||||
}
|
||||
|
||||
public Character[] getShortNames() {
|
||||
return shortNames;
|
||||
}
|
||||
|
||||
public ResolvableType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Integer getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public Integer getArityMin() {
|
||||
return arityMin;
|
||||
}
|
||||
|
||||
public Integer getArityMax() {
|
||||
return arityMax;
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultTargetSpec implements TargetSpec {
|
||||
|
||||
private BaseBuilder builder;
|
||||
private Object bean;
|
||||
private Method method;
|
||||
private Function<CommandContext, ?> function;
|
||||
private Consumer<CommandContext> consumer;
|
||||
|
||||
DefaultTargetSpec(BaseBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec method(Object bean, Method method) {
|
||||
this.bean = bean;
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec method(Object bean, String method, Class<?>... paramTypes) {
|
||||
this.bean = bean;
|
||||
this.method = ReflectionUtils.findMethod(bean.getClass(), method,
|
||||
ObjectUtils.isEmpty(paramTypes) ? null : paramTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec function(Function<CommandContext, ?> function) {
|
||||
this.function = function;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec consumer(Consumer<CommandContext> consumer) {
|
||||
this.consumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder and() {
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultCommandRegistration implements CommandRegistration {
|
||||
|
||||
private String command;
|
||||
private InteractionMode interactionMode;
|
||||
private String help;
|
||||
private String group;
|
||||
private String description;
|
||||
private Supplier<Availability> availability;
|
||||
private List<DefaultOptionSpec> optionSpecs;
|
||||
private DefaultTargetSpec targetSpec;
|
||||
|
||||
public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String help,
|
||||
String group, String description, Supplier<Availability> availability,
|
||||
List<DefaultOptionSpec> optionSpecs, DefaultTargetSpec targetSpec) {
|
||||
this.command = commandArrayToName(commands);
|
||||
this.interactionMode = interactionMode;
|
||||
this.help = help;
|
||||
this.group = group;
|
||||
this.description = description;
|
||||
this.availability = availability;
|
||||
this.optionSpecs = optionSpecs;
|
||||
this.targetSpec = targetSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionMode getInteractionMode() {
|
||||
return interactionMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelp() {
|
||||
return help;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Availability getAvailability() {
|
||||
return availability != null ? availability.get() : Availability.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandOption> getOptions() {
|
||||
return optionSpecs.stream()
|
||||
.map(o -> CommandOption.of(o.getLongNames(), o.getShortNames(), o.getDescription(), o.getType(),
|
||||
o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetInfo getTarget() {
|
||||
if (targetSpec.bean != null) {
|
||||
return TargetInfo.of(targetSpec.bean, targetSpec.method);
|
||||
}
|
||||
if (targetSpec.function != null) {
|
||||
return TargetInfo.of(targetSpec.function);
|
||||
}
|
||||
if (targetSpec.consumer != null) {
|
||||
return TargetInfo.of(targetSpec.consumer);
|
||||
}
|
||||
throw new IllegalArgumentException("No bean, function or consumer defined");
|
||||
}
|
||||
|
||||
private static String commandArrayToName(String[] commands) {
|
||||
return Arrays.asList(commands).stream()
|
||||
.flatMap(c -> Stream.of(c.split(" ")))
|
||||
.filter(c -> StringUtils.hasText(c))
|
||||
.map(c -> c.trim())
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultBuilder extends BaseBuilder {
|
||||
|
||||
}
|
||||
|
||||
static class BaseBuilder implements Builder {
|
||||
|
||||
private String[] commands;
|
||||
private InteractionMode interactionMode = InteractionMode.ALL;
|
||||
private String help;
|
||||
private String group;
|
||||
private String description;
|
||||
private Supplier<Availability> availability;
|
||||
private List<DefaultOptionSpec> optionSpecs = new ArrayList<>();
|
||||
private DefaultTargetSpec targetSpec;
|
||||
|
||||
@Override
|
||||
public Builder command(String... commands) {
|
||||
Assert.notNull(commands, "commands must be set");
|
||||
this.commands = Arrays.asList(commands).stream()
|
||||
.flatMap(c -> Stream.of(c.split(" ")))
|
||||
.filter(c -> StringUtils.hasText(c))
|
||||
.map(c -> c.trim())
|
||||
.collect(Collectors.toList())
|
||||
.toArray(new String[0]);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder interactionMode(InteractionMode mode) {
|
||||
this.interactionMode = mode != null ? mode : InteractionMode.ALL;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder help(String help) {
|
||||
this.help = help;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder group(String group) {
|
||||
this.group = group;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder availability(Supplier<Availability> availability) {
|
||||
this.availability = availability;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec withOption() {
|
||||
DefaultOptionSpec spec = new DefaultOptionSpec(this);
|
||||
optionSpecs.add(spec);
|
||||
return spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec withTarget() {
|
||||
DefaultTargetSpec spec = new DefaultTargetSpec(this);
|
||||
targetSpec = spec;
|
||||
return spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandRegistration build() {
|
||||
Assert.notNull(commands, "command cannot be empty");
|
||||
Assert.notNull(targetSpec, "target cannot be empty");
|
||||
Assert.state(!(targetSpec.bean != null && targetSpec.function != null), "only one target can exist");
|
||||
return new DefaultCommandRegistration(commands, interactionMode, help, group, description, availability,
|
||||
optionSpecs, targetSpec);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface to resolve currently existing commands. It is useful to have fully
|
||||
* dynamic set of commands which may exists only if some conditions in a running
|
||||
* shell are met. For example if shell is targeting arbitrary server environment
|
||||
* some commands may or may not exist depending on a runtime state.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandResolver {
|
||||
|
||||
/**
|
||||
* Resolve command registrations.
|
||||
*
|
||||
* @return command registrations
|
||||
*/
|
||||
List<CommandRegistration> resolve();
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
/*
|
||||
* 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.command.invocation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validator;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.annotation.SynthesizingMethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.shell.ParameterValidationException;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Encapsulates information about a handler method consisting of a
|
||||
* {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
|
||||
* Provides convenient access to method parameters, the method return value,
|
||||
* method annotations, etc.
|
||||
*
|
||||
* <p>The class may be created with a bean instance or with a bean name
|
||||
* (e.g. lazy-init bean, prototype bean). Use {@link #createWithResolvedBean()}
|
||||
* to obtain a {@code HandlerMethod} instance with a bean instance resolved
|
||||
* through the associated {@link BeanFactory}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class InvocableShellMethod {
|
||||
|
||||
/** Public for wrapping with fallback logger. */
|
||||
public static final Logger log = LoggerFactory.getLogger(InvocableShellMethod.class);
|
||||
|
||||
private static final Object[] EMPTY_ARGS = new Object[0];
|
||||
|
||||
private final Object bean;
|
||||
|
||||
@Nullable
|
||||
private final BeanFactory beanFactory;
|
||||
|
||||
private final Class<?> beanType;
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Method bridgedMethod;
|
||||
|
||||
private final MethodParameter[] parameters;
|
||||
|
||||
@Nullable
|
||||
private InvocableShellMethod resolvedFromHandlerMethod;
|
||||
|
||||
private ShellMethodArgumentResolverComposite resolvers = new ShellMethodArgumentResolverComposite();
|
||||
|
||||
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
private Validator validator;
|
||||
|
||||
/**
|
||||
* Create an instance from a bean instance and a method.
|
||||
*/
|
||||
public InvocableShellMethod(Object bean, Method method) {
|
||||
Assert.notNull(bean, "Bean is required");
|
||||
Assert.notNull(method, "Method is required");
|
||||
this.bean = bean;
|
||||
this.beanFactory = null;
|
||||
this.beanType = ClassUtils.getUserClass(bean);
|
||||
this.method = method;
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from a bean instance, method name, and parameter types.
|
||||
* @throws NoSuchMethodException when the method cannot be found
|
||||
*/
|
||||
public InvocableShellMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
|
||||
Assert.notNull(bean, "Bean is required");
|
||||
Assert.notNull(methodName, "Method name is required");
|
||||
this.bean = bean;
|
||||
this.beanFactory = null;
|
||||
this.beanType = ClassUtils.getUserClass(bean);
|
||||
this.method = bean.getClass().getMethod(methodName, parameterTypes);
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from a bean name, a method, and a {@code BeanFactory}.
|
||||
* The method {@link #createWithResolvedBean()} may be used later to
|
||||
* re-create the {@code HandlerMethod} with an initialized bean.
|
||||
*/
|
||||
public InvocableShellMethod(String beanName, BeanFactory beanFactory, Method method) {
|
||||
Assert.hasText(beanName, "Bean name is required");
|
||||
Assert.notNull(beanFactory, "BeanFactory is required");
|
||||
Assert.notNull(method, "Method is required");
|
||||
this.bean = beanName;
|
||||
this.beanFactory = beanFactory;
|
||||
Class<?> beanType = beanFactory.getType(beanName);
|
||||
if (beanType == null) {
|
||||
throw new IllegalStateException("Cannot resolve bean type for bean with name '" + beanName + "'");
|
||||
}
|
||||
this.beanType = ClassUtils.getUserClass(beanType);
|
||||
this.method = method;
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor for use in subclasses.
|
||||
*/
|
||||
protected InvocableShellMethod(InvocableShellMethod handlerMethod) {
|
||||
Assert.notNull(handlerMethod, "HandlerMethod is required");
|
||||
this.bean = handlerMethod.bean;
|
||||
this.beanFactory = handlerMethod.beanFactory;
|
||||
this.beanType = handlerMethod.beanType;
|
||||
this.method = handlerMethod.method;
|
||||
this.bridgedMethod = handlerMethod.bridgedMethod;
|
||||
this.parameters = handlerMethod.parameters;
|
||||
this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-create HandlerMethod with the resolved handler.
|
||||
*/
|
||||
private InvocableShellMethod(InvocableShellMethod handlerMethod, Object handler) {
|
||||
Assert.notNull(handlerMethod, "HandlerMethod is required");
|
||||
Assert.notNull(handler, "Handler object is required");
|
||||
this.bean = handler;
|
||||
this.beanFactory = handlerMethod.beanFactory;
|
||||
this.beanType = handlerMethod.beanType;
|
||||
this.method = handlerMethod.method;
|
||||
this.bridgedMethod = handlerMethod.bridgedMethod;
|
||||
this.parameters = handlerMethod.parameters;
|
||||
this.resolvedFromHandlerMethod = handlerMethod;
|
||||
}
|
||||
|
||||
public void setValidator(Validator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use to use for resolving method argument values.
|
||||
*/
|
||||
public void setMessageMethodArgumentResolvers(ShellMethodArgumentResolverComposite argumentResolvers) {
|
||||
this.resolvers = argumentResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ParameterNameDiscoverer for resolving parameter names when needed
|
||||
* (e.g. default request attribute name).
|
||||
* <p>Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}.
|
||||
*/
|
||||
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
this.parameterNameDiscoverer = parameterNameDiscoverer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the method after resolving its argument values in the context of the given message.
|
||||
* <p>Argument values are commonly resolved through
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
* The {@code providedArgs} parameter however may supply argument values to be used directly,
|
||||
* i.e. without argument resolution.
|
||||
* <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
|
||||
* resolved arguments.
|
||||
* @param message the current message being processed
|
||||
* @param providedArgs "given" arguments matched by type, not resolved
|
||||
* @return the raw value returned by the invoked method
|
||||
* @throws Exception raised if no suitable argument resolver can be found,
|
||||
* or if the method raised an exception
|
||||
* @see #getMethodArgumentValues
|
||||
* @see #doInvoke
|
||||
*/
|
||||
@Nullable
|
||||
public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
|
||||
Object[] args = getMethodArgumentValues(message, providedArgs);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Arguments: " + Arrays.toString(args));
|
||||
}
|
||||
return doInvoke(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method argument values for the current message, checking the provided
|
||||
* argument values and falling back to the configured argument resolvers.
|
||||
* <p>The resulting array will be passed into {@link #doInvoke}.
|
||||
*/
|
||||
protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
|
||||
ConversionService conversionService = new DefaultConversionService();
|
||||
MethodParameter[] parameters = getMethodParameters();
|
||||
if (ObjectUtils.isEmpty(parameters)) {
|
||||
return EMPTY_ARGS;
|
||||
}
|
||||
|
||||
ResolvedHolder[] holders = new ResolvedHolder[parameters.length];
|
||||
|
||||
int unresolvedCount = 0;
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
MethodParameter parameter = parameters[i];
|
||||
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
||||
boolean supports = this.resolvers.supportsParameter(parameter);
|
||||
Object arg = null;
|
||||
if (supports) {
|
||||
arg = this.resolvers.resolveArgument(parameter, message);
|
||||
}
|
||||
else {
|
||||
unresolvedCount++;
|
||||
}
|
||||
holders[i] = new ResolvedHolder(supports, parameter, arg);
|
||||
}
|
||||
|
||||
Object[] args = new Object[parameters.length];
|
||||
int providedArgsIndex = 0;
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
if (!holders[i].resolved) {
|
||||
if (providedArgs != null && unresolvedCount <= providedArgs.length) {
|
||||
if (conversionService.canConvert(providedArgs[providedArgsIndex].getClass(), holders[i].parameter.getParameterType())) {
|
||||
holders[i].arg = conversionService.convert(providedArgs[providedArgsIndex], holders[i].parameter.getParameterType());
|
||||
providedArgsIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
args[i] = holders[i].arg;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private static class ResolvedHolder {
|
||||
boolean resolved;
|
||||
MethodParameter parameter;
|
||||
Object arg;
|
||||
|
||||
public ResolvedHolder(boolean resolved, MethodParameter parameter, Object arg) {
|
||||
this.resolved = resolved;
|
||||
this.parameter = parameter;
|
||||
this.arg = arg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler method with the given argument values.
|
||||
*/
|
||||
@Nullable
|
||||
protected Object doInvoke(Object... args) throws Exception {
|
||||
try {
|
||||
if (validator != null) {
|
||||
Method bridgedMethod = getBridgedMethod();
|
||||
Validator validator = Utils.defaultValidator();
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables()
|
||||
.validateParameters(getBean(), bridgedMethod, args);
|
||||
if (constraintViolations.size() > 0) {
|
||||
throw new ParameterValidationException(constraintViolations);
|
||||
}
|
||||
}
|
||||
return getBridgedMethod().invoke(getBean(), args);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
assertTargetBean(getBridgedMethod(), getBean(), args);
|
||||
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
|
||||
throw new IllegalStateException(formatInvokeError(text, args), ex);
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
// Unwrap for HandlerExceptionResolvers ...
|
||||
Throwable targetException = ex.getTargetException();
|
||||
if (targetException instanceof RuntimeException) {
|
||||
throw (RuntimeException) targetException;
|
||||
}
|
||||
else if (targetException instanceof Error) {
|
||||
throw (Error) targetException;
|
||||
}
|
||||
else if (targetException instanceof Exception) {
|
||||
throw (Exception) targetException;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) {
|
||||
return new AsyncResultMethodParameter(returnValue);
|
||||
}
|
||||
|
||||
private MethodParameter[] initMethodParameters() {
|
||||
int count = this.bridgedMethod.getParameterCount();
|
||||
MethodParameter[] result = new MethodParameter[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
result[i] = new HandlerMethodParameter(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bean for this handler method.
|
||||
*/
|
||||
public Object getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method for this handler method.
|
||||
*/
|
||||
public Method getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the type of the handler for this handler method.
|
||||
* <p>Note that if the bean type is a CGLIB-generated class, the original
|
||||
* user-defined class is returned.
|
||||
*/
|
||||
public Class<?> getBeanType() {
|
||||
return this.beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the bean method is a bridge method, this method returns the bridged
|
||||
* (user-defined) method. Otherwise it returns the same method as {@link #getMethod()}.
|
||||
*/
|
||||
protected Method getBridgedMethod() {
|
||||
return this.bridgedMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method parameters for this handler method.
|
||||
*/
|
||||
public MethodParameter[] getMethodParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HandlerMethod return type.
|
||||
*/
|
||||
public MethodParameter getReturnType() {
|
||||
return new HandlerMethodParameter(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual return value type.
|
||||
*/
|
||||
public MethodParameter getReturnValueType(@Nullable Object returnValue) {
|
||||
return new ReturnValueMethodParameter(returnValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the method return type is void, {@code false} otherwise.
|
||||
*/
|
||||
public boolean isVoid() {
|
||||
return Void.TYPE.equals(getReturnType().getParameterType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single annotation on the underlying method traversing its super methods
|
||||
* if no annotation can be found on the given method itself.
|
||||
* <p>Also supports <em>merged</em> composed annotations with attribute
|
||||
* overrides.
|
||||
* @param annotationType the type of annotation to introspect the method for
|
||||
* @return the annotation, or {@code null} if none found
|
||||
* @see AnnotatedElementUtils#findMergedAnnotation
|
||||
*/
|
||||
@Nullable
|
||||
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
|
||||
return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the parameter is declared with the given annotation type.
|
||||
* @param annotationType the annotation type to look for
|
||||
* @see AnnotatedElementUtils#hasAnnotation
|
||||
*/
|
||||
public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
|
||||
return AnnotatedElementUtils.hasAnnotation(this.method, annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HandlerMethod from which this HandlerMethod instance was
|
||||
* resolved via {@link #createWithResolvedBean()}.
|
||||
*/
|
||||
@Nullable
|
||||
public InvocableShellMethod getResolvedFromHandlerMethod() {
|
||||
return this.resolvedFromHandlerMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the provided instance contains a bean name rather than an object instance,
|
||||
* the bean name is resolved before a {@link HandlerMethod} is created and returned.
|
||||
*/
|
||||
public InvocableShellMethod createWithResolvedBean() {
|
||||
Object handler = this.bean;
|
||||
if (this.bean instanceof String) {
|
||||
Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
|
||||
String beanName = (String) this.bean;
|
||||
handler = this.beanFactory.getBean(beanName);
|
||||
}
|
||||
return new InvocableShellMethod(this, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a short representation of this handler method for log message purposes.
|
||||
*/
|
||||
public String getShortLogMessage() {
|
||||
int args = this.method.getParameterCount();
|
||||
return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof InvocableShellMethod)) {
|
||||
return false;
|
||||
}
|
||||
InvocableShellMethod otherMethod = (InvocableShellMethod) other;
|
||||
return (this.bean.equals(otherMethod.bean) && this.method.equals(otherMethod.method));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (this.bean.hashCode() * 31 + this.method.hashCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.method.toGenericString();
|
||||
}
|
||||
|
||||
|
||||
// Support methods for use in "InvocableHandlerMethod" sub-class variants..
|
||||
|
||||
@Nullable
|
||||
protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) {
|
||||
if (!ObjectUtils.isEmpty(providedArgs)) {
|
||||
for (Object providedArg : providedArgs) {
|
||||
if (parameter.getParameterType().isInstance(providedArg)) {
|
||||
return providedArg;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static String formatArgumentError(MethodParameter param, String message) {
|
||||
return "Could not resolve parameter [" + param.getParameterIndex() + "] in " +
|
||||
param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the target bean class is an instance of the class where the given
|
||||
* method is declared. In some cases the actual endpoint instance at request-
|
||||
* processing time may be a JDK dynamic proxy (lazy initialization, prototype
|
||||
* beans, and others). Endpoint classes that require proxying should prefer
|
||||
* class-based proxy mechanisms.
|
||||
*/
|
||||
protected void assertTargetBean(Method method, Object targetBean, Object[] args) {
|
||||
Class<?> methodDeclaringClass = method.getDeclaringClass();
|
||||
Class<?> targetBeanClass = targetBean.getClass();
|
||||
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
|
||||
String text = "The mapped handler method class '" + methodDeclaringClass.getName() +
|
||||
"' is not an instance of the actual endpoint bean class '" +
|
||||
targetBeanClass.getName() + "'. If the endpoint requires proxying " +
|
||||
"(e.g. due to @Transactional), please use class-based proxying.";
|
||||
throw new IllegalStateException(formatInvokeError(text, args));
|
||||
}
|
||||
}
|
||||
|
||||
protected String formatInvokeError(String text, Object[] args) {
|
||||
|
||||
String formattedArgs = IntStream.range(0, args.length)
|
||||
.mapToObj(i -> (args[i] != null ?
|
||||
"[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" :
|
||||
"[" + i + "] [null]"))
|
||||
.collect(Collectors.joining(",\n", " ", " "));
|
||||
|
||||
return text + "\n" +
|
||||
"Endpoint [" + getBeanType().getName() + "]\n" +
|
||||
"Method [" + getBridgedMethod().toGenericString() + "] " +
|
||||
"with argument values:\n" + formattedArgs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A MethodParameter with HandlerMethod-specific behavior.
|
||||
*/
|
||||
protected class HandlerMethodParameter extends SynthesizingMethodParameter {
|
||||
|
||||
public HandlerMethodParameter(int index) {
|
||||
super(InvocableShellMethod.this.bridgedMethod, index);
|
||||
}
|
||||
|
||||
protected HandlerMethodParameter(HandlerMethodParameter original) {
|
||||
super(original);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getContainingClass() {
|
||||
return InvocableShellMethod.this.getBeanType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) {
|
||||
return InvocableShellMethod.this.getMethodAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) {
|
||||
return InvocableShellMethod.this.hasMethodAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerMethodParameter clone() {
|
||||
return new HandlerMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A MethodParameter for a HandlerMethod return type based on an actual return value.
|
||||
*/
|
||||
private class ReturnValueMethodParameter extends HandlerMethodParameter {
|
||||
|
||||
@Nullable
|
||||
private final Object returnValue;
|
||||
|
||||
public ReturnValueMethodParameter(@Nullable Object returnValue) {
|
||||
super(-1);
|
||||
this.returnValue = returnValue;
|
||||
}
|
||||
|
||||
protected ReturnValueMethodParameter(ReturnValueMethodParameter original) {
|
||||
super(original);
|
||||
this.returnValue = original.returnValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getParameterType() {
|
||||
return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReturnValueMethodParameter clone() {
|
||||
return new ReturnValueMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncResultMethodParameter extends HandlerMethodParameter {
|
||||
|
||||
@Nullable
|
||||
private final Object returnValue;
|
||||
|
||||
private final ResolvableType returnType;
|
||||
|
||||
public AsyncResultMethodParameter(@Nullable Object returnValue) {
|
||||
super(-1);
|
||||
this.returnValue = returnValue;
|
||||
this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
|
||||
}
|
||||
|
||||
protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
|
||||
super(original);
|
||||
this.returnValue = original.returnValue;
|
||||
this.returnType = original.returnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getParameterType() {
|
||||
if (this.returnValue != null) {
|
||||
return this.returnValue.getClass();
|
||||
}
|
||||
if (!ResolvableType.NONE.equals(this.returnType)) {
|
||||
return this.returnType.toClass();
|
||||
}
|
||||
return super.getParameterType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getGenericParameterType() {
|
||||
return this.returnType.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncResultMethodParameter clone() {
|
||||
return new AsyncResultMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.command.invocation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
|
||||
/**
|
||||
* Resolves method parameters by delegating to a list of registered
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
* Previously resolved method parameters are cached for faster lookups.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
public class ShellMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
|
||||
|
||||
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
|
||||
new ConcurrentHashMap<>(256);
|
||||
|
||||
|
||||
/**
|
||||
* Add the given {@link HandlerMethodArgumentResolver}.
|
||||
*/
|
||||
public ShellMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) {
|
||||
this.argumentResolvers.add(resolver);
|
||||
AnnotationAwareOrderComparator.sort(this.argumentResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
* @since 4.3
|
||||
*/
|
||||
public ShellMethodArgumentResolverComposite addResolvers(
|
||||
@Nullable HandlerMethodArgumentResolver... resolvers) {
|
||||
|
||||
if (resolvers != null) {
|
||||
Collections.addAll(this.argumentResolvers, resolvers);
|
||||
}
|
||||
AnnotationAwareOrderComparator.sort(this.argumentResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
*/
|
||||
public ShellMethodArgumentResolverComposite addResolvers(
|
||||
@Nullable List<? extends HandlerMethodArgumentResolver> resolvers) {
|
||||
|
||||
if (resolvers != null) {
|
||||
this.argumentResolvers.addAll(resolvers);
|
||||
}
|
||||
AnnotationAwareOrderComparator.sort(this.argumentResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a read-only list with the contained resolvers, or an empty list.
|
||||
*/
|
||||
public List<HandlerMethodArgumentResolver> getResolvers() {
|
||||
return Collections.unmodifiableList(this.argumentResolvers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the list of configured resolvers and the resolver cache.
|
||||
*/
|
||||
public void clear() {
|
||||
this.argumentResolvers.clear();
|
||||
this.argumentResolverCache.clear();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether the given {@linkplain MethodParameter method parameter} is
|
||||
* supported by any registered {@link HandlerMethodArgumentResolver}.
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return getArgumentResolver(parameter) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over registered
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}
|
||||
* and invoke the one that supports it.
|
||||
* @throws IllegalArgumentException if no suitable argument resolver is found
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
|
||||
if (resolver == null) {
|
||||
throw new IllegalArgumentException("Unsupported parameter type [" +
|
||||
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
|
||||
}
|
||||
return resolver.resolveArgument(parameter, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a registered {@link HandlerMethodArgumentResolver} that supports
|
||||
* the given method parameter.
|
||||
*/
|
||||
@Nullable
|
||||
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
|
||||
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
|
||||
if (result == null) {
|
||||
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
|
||||
if (resolver.supportsParameter(parameter)) {
|
||||
result = resolver;
|
||||
this.argumentResolverCache.put(parameter, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.completion;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
/**
|
||||
* Interface resolving completion proposals.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CompletionResolver {
|
||||
|
||||
/**
|
||||
* Resolve completions.
|
||||
*
|
||||
* @param registration the command registration
|
||||
* @param context the completion context
|
||||
* @return list of resolved completions
|
||||
*/
|
||||
List<CompletionProposal> resolve(CommandRegistration registration, CompletionContext context);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.completion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CompletionResolver}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class DefaultCompletionResolver implements CompletionResolver {
|
||||
|
||||
@Override
|
||||
public List<CompletionProposal> resolve(CommandRegistration registration, CompletionContext context) {
|
||||
List<CompletionProposal> candidates = new ArrayList<>();
|
||||
registration.getOptions().stream()
|
||||
.flatMap(o -> Stream.of(o.getLongNames()))
|
||||
.map(ln -> new CompletionProposal("--" + ln))
|
||||
.forEach(candidates::add);
|
||||
registration.getOptions().stream()
|
||||
.flatMap(o -> Stream.of(o.getShortNames()))
|
||||
.map(ln -> new CompletionProposal("-" + ln))
|
||||
.forEach(candidates::add);
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;;
|
||||
|
||||
/**
|
||||
* Displays command parsing errors on the terminal.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class CommandParserExceptionsExceptionResultHandler extends TerminalAwareResultHandler<CommandParserExceptionsException> {
|
||||
|
||||
public CommandParserExceptionsExceptionResultHandler(Terminal terminal) {
|
||||
super(terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doHandleResult(CommandParserExceptionsException result) {
|
||||
AttributedStringBuilder builder = new AttributedStringBuilder();
|
||||
result.getParserExceptions().stream().forEach(e -> {
|
||||
builder.append(new AttributedString(e.getMessage(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
builder.append("\n");
|
||||
});
|
||||
terminal.writer().append(builder.toAnsi());
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -13,33 +13,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import javax.validation.ElementKind;
|
||||
import javax.validation.Path;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.ParameterValidationException;
|
||||
import org.springframework.shell.Utils;
|
||||
|
||||
/**
|
||||
* Displays validation errors on the terminal.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ParameterValidationExceptionResultHandler
|
||||
extends TerminalAwareResultHandler<ParameterValidationException> {
|
||||
@@ -48,48 +34,15 @@ public class ParameterValidationExceptionResultHandler
|
||||
super(terminal);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private List<ParameterResolver> parameterResolvers;
|
||||
|
||||
@Override
|
||||
protected void doHandleResult(ParameterValidationException result) {
|
||||
terminal.writer().println(new AttributedString("The following constraints were not met:",
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
result.getConstraintViolations().stream()
|
||||
.forEach(v -> {
|
||||
Optional<Integer> parameterIndex = StreamSupport.stream(v.getPropertyPath().spliterator(), false)
|
||||
.filter(n -> n.getKind() == ElementKind.PARAMETER)
|
||||
.map(n -> ((Path.ParameterNode) n).getParameterIndex())
|
||||
.findFirst();
|
||||
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(result.getMethodTarget().getMethod(),
|
||||
parameterIndex.get());
|
||||
List<ParameterDescription> descriptions = findParameterResolver(methodParameter)
|
||||
.describe(methodParameter).collect(Collectors.toList());
|
||||
if (descriptions.size() == 1) {
|
||||
ParameterDescription description = descriptions.get(0);
|
||||
AttributedStringBuilder ansi = new AttributedStringBuilder(100);
|
||||
ansi.append("\t").append(description.keys().get(0), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).bold());
|
||||
ansi.append(" ").append(description.formal(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).underline());
|
||||
String msg = String.format(" : %s (You passed '%s')",
|
||||
v.getMessage(),
|
||||
String.valueOf(v.getInvalidValue())
|
||||
);
|
||||
ansi.append(msg, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
|
||||
|
||||
terminal.writer().println(ansi.toAnsi(terminal));
|
||||
}
|
||||
// Several formals for one method param, must be framework like JCommander, etc
|
||||
else {
|
||||
// Output toString() for now...
|
||||
terminal.writer().println(new AttributedString(v.toString(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi(terminal));
|
||||
}
|
||||
|
||||
terminal.writer().println(new AttributedString(v.toString(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi(terminal));
|
||||
});
|
||||
}
|
||||
|
||||
private ParameterResolver findParameterResolver(MethodParameter methodParameter) {
|
||||
return parameterResolvers.stream().filter(pr -> pr.supports(methodParameter)).findFirst().get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 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.
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
@@ -22,8 +21,8 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.TerminalSizeAware;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.jline.InteractiveShellRunner;
|
||||
|
||||
/**
|
||||
@@ -57,9 +56,13 @@ public class ResultHandlerConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThrowableResultHandler throwableResultHandler(Terminal terminal, CommandRegistry commandRegistry,
|
||||
ObjectProvider<InteractiveShellRunner> interactiveApplicationRunner) {
|
||||
return new ThrowableResultHandler(terminal, commandRegistry, interactiveApplicationRunner);
|
||||
public CommandParserExceptionsExceptionResultHandler commandParserExceptionsExceptionResultHandler(Terminal terminal) {
|
||||
return new CommandParserExceptionsExceptionResultHandler(terminal);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThrowableResultHandler throwableResultHandler(Terminal terminal, CommandCatalog commandCatalog,
|
||||
ObjectProvider<InteractiveShellRunner> interactiveApplicationRunner) {
|
||||
return new ThrowableResultHandler(terminal, commandCatalog, interactiveApplicationRunner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Copyright 2015-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.
|
||||
@@ -22,8 +22,8 @@ import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.jline.InteractiveShellRunner;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -43,14 +43,14 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable
|
||||
|
||||
private Throwable lastError;
|
||||
|
||||
private CommandRegistry commandRegistry;
|
||||
private CommandCatalog commandCatalog;
|
||||
|
||||
private ObjectProvider<InteractiveShellRunner> interactiveRunner;
|
||||
|
||||
public ThrowableResultHandler(Terminal terminal, CommandRegistry commandRegistry,
|
||||
public ThrowableResultHandler(Terminal terminal, CommandCatalog commandCatalog,
|
||||
ObjectProvider<InteractiveShellRunner> interactiveRunner) {
|
||||
super(terminal);
|
||||
this.commandRegistry = commandRegistry;
|
||||
this.commandCatalog = commandCatalog;
|
||||
this.interactiveRunner = interactiveRunner;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable
|
||||
String toPrint = StringUtils.hasLength(result.getMessage()) ? result.getMessage() : result.toString();
|
||||
terminal.writer().println(new AttributedString(toPrint,
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
if (interactiveRunner.getIfAvailable() != null && commandRegistry.listCommands().containsKey(DETAILS_COMMAND_NAME)) {
|
||||
if (interactiveRunner.getIfAvailable() != null && commandCatalog.getRegistrations().keySet().contains(DETAILS_COMMAND_NAME)) {
|
||||
terminal.writer().println(
|
||||
new AttributedStringBuilder()
|
||||
.append("Details of the error have been omitted. You can use the ", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED))
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanExpressionContext;
|
||||
import org.springframework.beans.factory.config.BeanExpressionResolver;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.annotation.ValueConstants;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class to resolve method arguments from a named value, e.g.
|
||||
* message headers or destination variables. Named values could have one or more
|
||||
* of a name, a required flag, and a default value.
|
||||
*
|
||||
* <p>Subclasses only need to define specific steps such as how to obtain named
|
||||
* value details from a method parameter, how to resolve to argument values, or
|
||||
* how to handle missing values.
|
||||
*
|
||||
* <p>A default value string can contain ${...} placeholders and Spring
|
||||
* Expression Language {@code #{...}} expressions which will be resolved if a
|
||||
* {@link ConfigurableBeanFactory} is supplied to the class constructor.
|
||||
*
|
||||
* <p>A {@link ConversionService} is used to convert a resolved String argument
|
||||
* value to the expected target method parameter type.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
public static final String ARGUMENT_PREFIX = "springShellArgument.";
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
@Nullable
|
||||
private final ConfigurableBeanFactory configurableBeanFactory;
|
||||
|
||||
@Nullable
|
||||
private final BeanExpressionContext expressionContext;
|
||||
|
||||
private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
|
||||
|
||||
/**
|
||||
* Constructor with a {@link ConversionService} and a {@link BeanFactory}.
|
||||
* @param conversionService conversion service for converting String values
|
||||
* to the target method parameter type
|
||||
* @param beanFactory a bean factory for resolving {@code ${...}}
|
||||
* placeholders and {@code #{...}} SpEL expressions in default values
|
||||
*/
|
||||
protected AbstractArgumentMethodArgumentResolver(ConversionService conversionService,
|
||||
@Nullable ConfigurableBeanFactory beanFactory) {
|
||||
|
||||
// Fallback on shared ConversionService for now for historic reasons.
|
||||
// Possibly remove after discussion in gh-23882.
|
||||
|
||||
//noinspection ConstantConditions
|
||||
this.conversionService = (conversionService != null ?
|
||||
conversionService : DefaultConversionService.getSharedInstance());
|
||||
|
||||
this.configurableBeanFactory = beanFactory;
|
||||
this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
|
||||
MethodParameter nestedParameter = parameter.nestedIfOptional();
|
||||
|
||||
Object arg = resolveArgumentInternal(nestedParameter, message, namedValueInfo.names);
|
||||
if (arg == null) {
|
||||
if (namedValueInfo.defaultValue != null) {
|
||||
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
|
||||
}
|
||||
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
|
||||
handleMissingValue(namedValueInfo.names, nestedParameter, message);
|
||||
}
|
||||
arg = handleNullValue(namedValueInfo.names, arg, nestedParameter.getNestedParameterType());
|
||||
}
|
||||
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
|
||||
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
|
||||
}
|
||||
|
||||
if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) {
|
||||
arg = this.conversionService.convert(arg, TypeDescriptor.forObject(arg), new TypeDescriptor(parameter));
|
||||
// Check for null value after conversion of incoming argument value
|
||||
if (arg == null && namedValueInfo.defaultValue == null &&
|
||||
namedValueInfo.required && !nestedParameter.isOptional()) {
|
||||
handleMissingValue(namedValueInfo.names, nestedParameter, message);
|
||||
}
|
||||
}
|
||||
|
||||
handleResolvedValue(arg, namedValueInfo.names, parameter, message);
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the named value for the given method parameter.
|
||||
*/
|
||||
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
|
||||
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
|
||||
if (namedValueInfo == null) {
|
||||
namedValueInfo = createNamedValueInfo(parameter);
|
||||
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
|
||||
this.namedValueInfoCache.put(parameter, namedValueInfo);
|
||||
}
|
||||
return namedValueInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the {@link NamedValueInfo} object for the given method parameter.
|
||||
* Implementations typically retrieve the method annotation by means of
|
||||
* {@link MethodParameter#getParameterAnnotation(Class)}.
|
||||
* @param parameter the method parameter
|
||||
* @return the named value information
|
||||
*/
|
||||
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Fall back on the parameter name from the class file if necessary and
|
||||
* replace {@link ValueConstants#DEFAULT_NONE} with null.
|
||||
*/
|
||||
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
|
||||
List<String> names = info.names;
|
||||
if (info.names.isEmpty()) {
|
||||
String name = parameter.getParameterName();
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Name for argument of type [" + parameter.getNestedParameterType().getName() +
|
||||
"] not specified, and parameter name information not found in class file either.");
|
||||
}
|
||||
names.add(name);
|
||||
}
|
||||
return new NamedValueInfo(names, info.required,
|
||||
ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given annotation-specified value,
|
||||
* potentially containing placeholders and expressions.
|
||||
*/
|
||||
@Nullable
|
||||
private Object resolveEmbeddedValuesAndExpressions(String value) {
|
||||
if (this.configurableBeanFactory == null || this.expressionContext == null) {
|
||||
return value;
|
||||
}
|
||||
String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
|
||||
BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
|
||||
if (exprResolver == null) {
|
||||
return value;
|
||||
}
|
||||
return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the given parameter type and value name into an argument value.
|
||||
* @param parameter the method parameter to resolve to an argument value
|
||||
* @param message the current request
|
||||
* @param name the name of the value being resolved
|
||||
* @return the resolved argument. May be {@code null}
|
||||
* @throws Exception in case of errors
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, List<String> names)
|
||||
throws Exception;
|
||||
|
||||
/**
|
||||
* Invoked when a value is required, but {@link #resolveArgumentInternal}
|
||||
* returned {@code null} and there is no default value. Sub-classes can
|
||||
* throw an appropriate exception for this case.
|
||||
* @param names the name for the value
|
||||
* @param parameter the target method parameter
|
||||
* @param message the message being processed
|
||||
*/
|
||||
protected abstract void handleMissingValue(List<String> names, MethodParameter parameter, Message<?> message);
|
||||
|
||||
/**
|
||||
* One last chance to handle a possible null value.
|
||||
* Specifically for booleans method parameters, use {@link Boolean#FALSE}.
|
||||
* Also raise an ISE for primitive types.
|
||||
*/
|
||||
@Nullable
|
||||
private Object handleNullValue(List<String> name, @Nullable Object value, Class<?> paramType) {
|
||||
if (value == null) {
|
||||
if (Boolean.TYPE.equals(paramType)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
else if (paramType.isPrimitive()) {
|
||||
throw new IllegalStateException("Optional " + paramType + " parameter '" + name +
|
||||
"' is present but cannot be translated into a null value due to being " +
|
||||
"declared as a primitive type. Consider declaring it as object wrapper " +
|
||||
"for the corresponding primitive type.");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a value is resolved.
|
||||
* @param arg the resolved argument value
|
||||
* @param name the argument name
|
||||
* @param parameter the argument parameter type
|
||||
* @param message the message
|
||||
*/
|
||||
protected void handleResolvedValue(
|
||||
@Nullable Object arg, List<String> name, MethodParameter parameter, Message<?> message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a named value declaration.
|
||||
*/
|
||||
protected static class NamedValueInfo {
|
||||
|
||||
private final List<String> names;
|
||||
|
||||
private final boolean required;
|
||||
|
||||
@Nullable
|
||||
private final String defaultValue;
|
||||
|
||||
protected NamedValueInfo(List<String> names, boolean required, @Nullable String defaultValue) {
|
||||
this.names = names;
|
||||
this.required = required;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ConfigurableCommandRegistry}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ConfigurableCommandRegistryTest {
|
||||
|
||||
@Test
|
||||
public void testRegistration() {
|
||||
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
registry.register("foo", MethodTarget.of("toString", this, new Command.Help("some command")));
|
||||
assertThat(registry.listCommands()).containsKeys("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleRegistration() {
|
||||
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
registry.register("foo", MethodTarget.of("toString", this, new Command.Help("some command")));
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registry.register("foo", MethodTarget.of("hashCode", this, new Command.Help("some command")));
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("foo")
|
||||
.hasMessageContaining("toString")
|
||||
.hasMessageContaining("hashCode");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -13,12 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -31,6 +29,10 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -44,7 +46,7 @@ import static org.mockito.Mockito.when;
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ShellTest {
|
||||
public class ShellTests {
|
||||
|
||||
@Mock
|
||||
private InputProvider inputProvider;
|
||||
@@ -53,12 +55,10 @@ public class ShellTest {
|
||||
ResultHandlerService resultHandlerService;
|
||||
|
||||
@Mock
|
||||
CommandRegistry commandRegistry;
|
||||
CommandCatalog commandRegistry;
|
||||
|
||||
@Mock
|
||||
private ParameterResolver parameterResolver;
|
||||
|
||||
private ValueResult valueResult;
|
||||
private CompletionResolver completionResolver;
|
||||
|
||||
@InjectMocks
|
||||
private Shell shell;
|
||||
@@ -67,26 +67,30 @@ public class ShellTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
shell.parameterResolvers = Arrays.asList(parameterResolver);
|
||||
shell.setCompletionResolvers(Arrays.asList(completionResolver));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandMatch() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
|
||||
valueResult = new ValueResult(null, "test");
|
||||
when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(any());
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(Collections.singletonMap("hello world",
|
||||
MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
System.out.println(expected);
|
||||
}
|
||||
|
||||
assertThat(invoked).isTrue();
|
||||
@@ -97,8 +101,15 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(isA(CommandNotFound.class));
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(Collections.singletonMap("bonjour",
|
||||
MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("bonjour")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -115,8 +126,15 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "helloworld how are you doing ?");
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(isA(CommandNotFound.class));
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(
|
||||
Collections.singletonMap("hello", MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -129,14 +147,18 @@ public class ShellTest {
|
||||
|
||||
@Test
|
||||
public void noCommand() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "", () -> "hello world how are you doing ?", null);
|
||||
valueResult = new ValueResult(null, "test");
|
||||
when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(any());
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(Collections.singletonMap("hello world",
|
||||
MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -154,8 +176,16 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "fail");
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(isA(SomeException.class));
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(Collections.singletonMap("fail",
|
||||
MethodTarget.of("failing", this, new Command.Help("Will throw an exception"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("fail")
|
||||
.withTarget()
|
||||
.method(this, "failing")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("fail", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -177,17 +207,27 @@ public class ShellTest {
|
||||
|
||||
@Test
|
||||
public void commandNameCompletion() throws Exception {
|
||||
Map<String, MethodTarget> methodTargets = new HashMap<>();
|
||||
methodTargets.put("hello world", MethodTarget.of("helloWorld", this, new Command.Help("hellow world")));
|
||||
methodTargets.put("another command", MethodTarget.of("helloWorld", this, new Command.Help("another command")));
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(commandRegistry.listCommands()).thenReturn(methodTargets);
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
CommandRegistration registration2 = CommandRegistration.builder()
|
||||
.command("another command")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration1);
|
||||
registrations.put("another command", registration2);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
// Invoke at very start
|
||||
List<String> proposals = shell.complete(new CompletionContext(Arrays.asList(""), 0, "".length()))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(proposals).containsExactlyInAnyOrder("another command", "hello world");
|
||||
// assertThat(proposals).containsExactly("another command", "hello world");
|
||||
|
||||
// Invoke in middle of first word
|
||||
proposals = shell.complete(new CompletionContext(Arrays.asList("hel"), 0, "hel".length()))
|
||||
@@ -231,6 +271,51 @@ public class ShellTest {
|
||||
throw new SomeException();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completionArgWithMethod() throws Exception {
|
||||
when(completionResolver.resolve(any(), any())).thenReturn(Arrays.asList(new CompletionProposal("--arg1")));
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("arg1 desc")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration1);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
List<String> proposals = shell.complete(new CompletionContext(Arrays.asList("hello", "world", ""), 2, "".length()))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(proposals).containsExactlyInAnyOrder("--arg1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completionArgWithFunction() throws Exception {
|
||||
when(completionResolver.resolve(any(), any())).thenReturn(Arrays.asList(new CompletionProposal("--arg1")));
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.function(ctx -> {
|
||||
return null;
|
||||
})
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("arg1 desc")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration1);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
List<String> proposals = shell.complete(new CompletionContext(Arrays.asList("hello", "world", ""), 2, "".length()))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(proposals).containsExactlyInAnyOrder("--arg1");
|
||||
}
|
||||
|
||||
private static class Exit extends RuntimeException {
|
||||
}
|
||||
@@ -238,6 +323,4 @@ public class ShellTest {
|
||||
private static class SomeException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,37 +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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Utils}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class UtilsTest {
|
||||
|
||||
@Test
|
||||
public void testUnCamelify() throws Exception {
|
||||
assertThat(Utils.unCamelify("HelloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorldHowAreYou")).isEqualTo("hello-world-how-are-you");
|
||||
assertThat(Utils.unCamelify("URL")).isEqualTo("url");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2015-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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Utils}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class UtilsTests {
|
||||
|
||||
@Test
|
||||
public void testUnCamelify() throws Exception {
|
||||
assertThat(Utils.unCamelify("HelloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorldHowAreYou")).isEqualTo("hello-world-how-are-you");
|
||||
assertThat(Utils.unCamelify("URL")).isEqualTo("url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSplit() {
|
||||
Predicate<String> predicate = t -> t.startsWith("-");
|
||||
List<List<String>> split = null;
|
||||
|
||||
split = Utils.split(new String[] { "-a1", "a1" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("-a1", "a1"));
|
||||
|
||||
split = Utils.split(new String[] { "-a1", "a1", "-a2", "a2" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("-a1", "a1"), Arrays.asList("-a2", "a2"));
|
||||
|
||||
split = Utils.split(new String[] { "a0", "-a1", "a1" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("a0"), Arrays.asList("-a1", "a1"));
|
||||
|
||||
split = Utils.split(new String[] { "-a1", "-a2" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("-a1"), Arrays.asList("-a2"));
|
||||
|
||||
split = Utils.split(new String[] { "a1", "a2" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("a1", "a2"));
|
||||
|
||||
split = Utils.split(new String[] { "-a1", "a1", "a2" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("-a1", "a1", "a2"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
|
||||
public abstract class AbstractCommandTests {
|
||||
|
||||
protected Pojo1 pojo1;
|
||||
|
||||
protected Function<CommandContext, String> function1 = ctx -> {
|
||||
String arg1 = ctx.getOptionValue("arg1");
|
||||
return "hi" + arg1;
|
||||
};
|
||||
|
||||
protected Function<CommandContext, Void> function2 = ctx -> {
|
||||
return null;
|
||||
};
|
||||
|
||||
@BeforeEach
|
||||
public void setupAbstractCommandTests() {
|
||||
pojo1 = new Pojo1();
|
||||
}
|
||||
|
||||
protected static class Pojo1 {
|
||||
|
||||
public int method1Count;
|
||||
public CommandContext method1Ctx;
|
||||
public int method2Count;
|
||||
public int method3Count;
|
||||
public int method4Count;
|
||||
public String method4Arg1;
|
||||
public Boolean method5ArgA;
|
||||
public Boolean method5ArgB;
|
||||
public Boolean method5ArgC;
|
||||
public int method6Count;
|
||||
public String method6Arg1;
|
||||
public String method6Arg2;
|
||||
public String method6Arg3;
|
||||
public int method7Count;
|
||||
public int method7Arg1;
|
||||
public int method7Arg2;
|
||||
public int method7Arg3;
|
||||
public int method8Count;
|
||||
public float[] method8Arg1;
|
||||
|
||||
public void method1(CommandContext ctx) {
|
||||
method1Ctx = ctx;
|
||||
method1Count++;
|
||||
}
|
||||
|
||||
public String method2() {
|
||||
method2Count++;
|
||||
return "hi";
|
||||
}
|
||||
|
||||
public String method3(@Header("arg1") String arg1) {
|
||||
method3Count++;
|
||||
return "hi" + arg1;
|
||||
}
|
||||
|
||||
public String method4(String arg1) {
|
||||
method4Arg1 = arg1;
|
||||
method4Count++;
|
||||
return "hi" + arg1;
|
||||
}
|
||||
|
||||
public void method5(@Header("a") boolean a, @Header("b") boolean b, @Header("c") boolean c) {
|
||||
method5ArgA = a;
|
||||
method5ArgB = b;
|
||||
method5ArgC = c;
|
||||
}
|
||||
|
||||
public String method6(String arg1, String arg2, String arg3) {
|
||||
method6Arg1 = arg1;
|
||||
method6Arg2 = arg2;
|
||||
method6Arg3 = arg3;
|
||||
method6Count++;
|
||||
return "hi" + arg1 + arg2 + arg3;
|
||||
}
|
||||
|
||||
public void method7(int arg1, int arg2, int arg3) {
|
||||
method7Arg1 = arg1;
|
||||
method7Arg2 = arg2;
|
||||
method7Arg3 = arg3;
|
||||
method7Count++;
|
||||
}
|
||||
|
||||
public void method8(float[] arg1) {
|
||||
method8Arg1 = arg1;
|
||||
method8Count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class CommandCatalogTests extends AbstractCommandTests {
|
||||
|
||||
@Test
|
||||
public void testCommandCatalog () {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("group1 sub1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
CommandCatalog catalog = CommandCatalog.of();
|
||||
catalog.register(r1);
|
||||
assertThat(catalog.getRegistrations()).hasSize(1);
|
||||
catalog.unregister(r1);
|
||||
assertThat(catalog.getRegistrations()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolver() {
|
||||
// catalog itself would not have any registered command but
|
||||
// this custom resolver adds one which may dymanically go away.
|
||||
DynamicCommandResolver resolver = new DynamicCommandResolver();
|
||||
CommandCatalog catalog = CommandCatalog.of(Arrays.asList(resolver), null);
|
||||
assertThat(catalog.getRegistrations()).hasSize(1);
|
||||
resolver.enabled = false;
|
||||
assertThat(catalog.getRegistrations()).hasSize(0);
|
||||
}
|
||||
|
||||
class DynamicCommandResolver implements CommandResolver {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("group1 sub1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
boolean enabled = true;
|
||||
|
||||
@Override
|
||||
public List<CommandRegistration> resolve() {
|
||||
List<CommandRegistration> regs = new ArrayList<>();
|
||||
if (enabled) {
|
||||
regs.add(r1);
|
||||
}
|
||||
return regs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionArity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class CommandExecutionTests extends AbstractCommandTests {
|
||||
|
||||
private CommandExecution execution;
|
||||
|
||||
@BeforeEach
|
||||
public void setupCommandExecutionTests() {
|
||||
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
|
||||
resolvers.add(new ArgumentHeaderMethodArgumentResolver(new DefaultConversionService(), null));
|
||||
resolvers.add(new CommandContextMethodArgumentResolver());
|
||||
execution = CommandExecution.of(resolvers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFunctionExecution() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodExecution1() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
assertThat(pojo1.method3Count).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodExecution2() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method1")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
assertThat(pojo1.method1Count).isEqualTo(1);
|
||||
assertThat(pojo1.method1Ctx).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSinglePositionalArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value"});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSingleWithNamedArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value");
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMultiPositionalArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMultiPositionalArgsAll() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.ONE_OR_MORE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value1 myarg1value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMultipleArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.description("some arg2")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg3")
|
||||
.description("some arg3")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method6")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"--arg1", "myarg1value", "--arg2", "myarg2value", "--arg3", "myarg3value"});
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
assertThat(pojo1.method6Arg3).isEqualTo("myarg3value");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMethodMultipleIntArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.description("some arg2")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg3")
|
||||
.description("some arg3")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method7")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"--arg1", "1", "--arg2", "2", "--arg3", "3"});
|
||||
assertThat(pojo1.method7Count).isEqualTo(1);
|
||||
assertThat(pojo1.method7Arg1).isEqualTo(1);
|
||||
assertThat(pojo1.method7Arg2).isEqualTo(2);
|
||||
assertThat(pojo1.method7Arg3).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMultiplePositionalStringArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.description("some arg2")
|
||||
.position(1)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg3")
|
||||
.description("some arg3")
|
||||
.position(2)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method6")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"myarg1value", "myarg2value", "myarg3value"});
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
assertThat(pojo1.method6Arg3).isEqualTo("myarg3value");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"myarg1value --arg2 myarg2value --arg3 myarg3value",
|
||||
"--arg1 myarg1value myarg2value --arg3 myarg3value",
|
||||
"--arg1 myarg1value --arg2 myarg2value myarg3value"
|
||||
})
|
||||
public void testMethodMultiplePositionalStringArgsMixed(String arg) {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.description("some arg2")
|
||||
.position(1)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg3")
|
||||
.description("some arg3")
|
||||
.position(2)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method6")
|
||||
.and()
|
||||
.build();
|
||||
String[] args = arg.split(" ");
|
||||
// execution.evaluate(r1, new String[]{"myarg1value", "--arg2", "myarg2value", "--arg3", "myarg3value"});
|
||||
execution.evaluate(r1, args);
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
assertThat(pojo1.method6Arg3).isEqualTo("myarg3value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortCombinedWithoutValue() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('a')
|
||||
.description("short arg a")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('b')
|
||||
.description("short arg b")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('c')
|
||||
.description("short arg c")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method5")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"-abc"});
|
||||
assertThat(pojo1.method5ArgA).isTrue();
|
||||
assertThat(pojo1.method5ArgB).isTrue();
|
||||
assertThat(pojo1.method5ArgC).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortCombinedSomeHavingValue() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('a')
|
||||
.description("short arg a")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('b')
|
||||
.description("short arg b")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('c')
|
||||
.description("short arg c")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method5")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"-ac", "-b", "false"});
|
||||
assertThat(pojo1.method5ArgA).isTrue();
|
||||
assertThat(pojo1.method5ArgB).isFalse();
|
||||
assertThat(pojo1.method5ArgC).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFloatArrayOne() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.type(float[].class)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method8")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "0.1"});
|
||||
assertThat(pojo1.method8Count).isEqualTo(1);
|
||||
assertThat(pojo1.method8Arg1).isEqualTo(new float[]{0.1f});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFloatArrayTwo() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.type(float[].class)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method8")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "0.1", "0.2"});
|
||||
assertThat(pojo1.method8Count).isEqualTo(1);
|
||||
assertThat(pojo1.method8Arg1).isEqualTo(new float[]{0.1f, 0.2f});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultValueAsNull() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredArg() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
execution.evaluate(r1, new String[]{});
|
||||
}).isInstanceOf(CommandParserExceptionsException.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResults;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class CommandParserTests extends AbstractCommandTests {
|
||||
|
||||
private CommandParser parser;
|
||||
|
||||
@BeforeEach
|
||||
public void setupCommandParserTests() {
|
||||
parser = CommandParser.of();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyOptionsAndArgs() {
|
||||
CommandParserResults results = parser.parse(Collections.emptyList(), new String[0]);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongName() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortName() {
|
||||
CommandOption option1 = shortOption('a');
|
||||
CommandOption option2 = shortOption('b');
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"-a", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleArgs() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo", "--arg2", "bar"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleArgsWithMultiValues() {
|
||||
CommandOption option1 = longOption("arg1", null, false, null, 1, 2);
|
||||
CommandOption option2 = longOption("arg2", null, false, null, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo1", "foo2", "--arg2", "bar1", "bar2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo1 foo2");
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo("bar1 bar2");
|
||||
assertThat(results.positional()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBooleanWithoutArg() {
|
||||
ResolvableType type = ResolvableType.forType(boolean.class);
|
||||
CommandOption option1 = shortOption('v', type);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"-v"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBooleanWithArg() {
|
||||
ResolvableType type = ResolvableType.forType(boolean.class);
|
||||
CommandOption option1 = shortOption('v', type);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"-v", "false"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingRequiredOption() {
|
||||
CommandOption option1 = longOption("arg1", true);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.errors()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpaceInArgWithOneArg() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "foo bar"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpaceInArgWithMultipleArgs() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo bar", "--arg2", "hi"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo bar");
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo("hi");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgs() {
|
||||
String[] args = new String[]{"arg1", "arg2"};
|
||||
CommandParserResults results = parser.parse(Collections.emptyList(), args);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
assertThat(results.positional()).containsExactly("arg1", "arg2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgBeforeOption() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"foo", "--arg1", "value"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value");
|
||||
assertThat(results.positional()).containsExactly("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgAfterOption() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value");
|
||||
assertThat(results.positional()).containsExactly("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgWithoutOption() {
|
||||
CommandOption option1 = longOption("arg1", 0, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value foo");
|
||||
assertThat(results.positional()).containsExactly("value", "foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgWithoutOptionHavingType() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(String.class), false, 0, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value foo");
|
||||
assertThat(results.positional()).containsExactly("value", "foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombined() {
|
||||
CommandOption optionA = shortOption('a');
|
||||
CommandOption optionB = shortOption('b');
|
||||
CommandOption optionC = shortOption('c');
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isNull();
|
||||
assertThat(results.results().get(1).value()).isNull();
|
||||
assertThat(results.results().get(2).value()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanType() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanTypeArgFalse() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc", "false"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(false);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(false);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanTypeSomeArgFalse() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-ac", "-b", "false"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongOptionsWithArray() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(int[].class));
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(new String[] { "1", "2" });
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapPositionalArgs1() {
|
||||
CommandOption option1 = longOption("arg1", 0, 1, 1);
|
||||
CommandOption option2 = longOption("arg2", 1, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("1");
|
||||
assertThat(results.results().get(1).value()).isEqualTo("2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapPositionalArgs2() {
|
||||
CommandOption option1 = longOption("arg1", 0, 1, 1);
|
||||
CommandOption option2 = longOption("arg2", 1, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("1");
|
||||
assertThat(results.results().get(1).value()).isEqualTo("2");
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name) {
|
||||
return longOption(name, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, boolean required) {
|
||||
return longOption(name, null, required, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type) {
|
||||
return longOption(name, type, false, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, int position, int arityMin, int arityMax) {
|
||||
return longOption(name, null, false, position, arityMin, arityMax);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type, boolean required, Integer position) {
|
||||
return longOption(name, type, required, position, null, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type, boolean required, Integer position, Integer arityMin, Integer arityMax) {
|
||||
return CommandOption.of(new String[] { name }, new Character[0], "desc", type, required, null, position,
|
||||
arityMin, arityMax);
|
||||
}
|
||||
|
||||
private static CommandOption shortOption(char name) {
|
||||
return shortOption(name, null);
|
||||
}
|
||||
|
||||
private static CommandOption shortOption(char name, ResolvableType type) {
|
||||
return CommandOption.of(new String[0], new Character[] { name }, "desc", type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionArity;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo.TargetType;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class CommandRegistrationTests extends AbstractCommandTests {
|
||||
|
||||
@Test
|
||||
public void testCommandMustBeSet() {
|
||||
assertThatThrownBy(() -> {
|
||||
CommandRegistration.builder().build();
|
||||
}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("command cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasics() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getGroup()).isNull();
|
||||
assertThat(registration.getInteractionMode()).isEqualTo(InteractionMode.ALL);
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.interactionMode(InteractionMode.NONINTERACTIVE)
|
||||
.group("fakegroup")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getInteractionMode()).isEqualTo(InteractionMode.NONINTERACTIVE);
|
||||
assertThat(registration.getGroup()).isEqualTo("fakegroup");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandStructures() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1", "command2")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1 command2");
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1 command2")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1 command2");
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command(" command1 command2 ")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1 command2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFunctionRegistration() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getTarget().getTargetType()).isEqualTo(TargetType.FUNCTION);
|
||||
assertThat(registration.getTarget().getFunction()).isNotNull();
|
||||
assertThat(registration.getTarget().getBean()).isNull();
|
||||
assertThat(registration.getTarget().getMethod()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumerRegistration() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getTarget().getTargetType()).isEqualTo(TargetType.CONSUMER);
|
||||
assertThat(registration.getTarget().getFunction()).isNull();
|
||||
assertThat(registration.getTarget().getConsumer()).isNotNull();
|
||||
assertThat(registration.getTarget().getBean()).isNull();
|
||||
assertThat(registration.getTarget().getMethod()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodRegistration() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getTarget().getTargetType()).isEqualTo(TargetType.METHOD);
|
||||
assertThat(registration.getTarget().getFunction()).isNull();
|
||||
assertThat(registration.getTarget().getBean()).isNotNull();
|
||||
assertThat(registration.getTarget().getMethod()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanUseOnlyOneTarget() {
|
||||
assertThatThrownBy(() -> {
|
||||
CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
}).isInstanceOf(IllegalStateException.class).hasMessageContaining("only one target can exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleFullRegistrationWithFunction() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getLongNames()).containsExactly("arg1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleFullRegistrationWithMethod() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionWithType() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getShortNames()).containsExactly('v');
|
||||
assertThat(registration.getOptions().get(0).getType()).isEqualTo(ResolvableType.forType(boolean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionWithRequired() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.required(true)
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getShortNames()).containsExactly('v');
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isTrue();
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.required(false)
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isFalse();
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isFalse();
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionWithDefaultValue() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.defaultValue("defaultValue")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getDefaultValue()).isEqualTo("defaultValue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionWithPositionValue() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.position(1)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getOptions()).hasSize(2);
|
||||
assertThat(registration.getOptions().get(0).getPosition()).isEqualTo(1);
|
||||
assertThat(registration.getOptions().get(1).getPosition()).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArityViaInts() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.arity(0, 0)
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
|
||||
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArityViaEnum() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.arity(OptionArity.ZERO)
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
|
||||
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user