Add dynamic command availability
Introduce availability concept on MethodTarget (with reason if not available) Add bridge to @CliAvailabilityIndicator to Legacy registrar Fixes #138 Add help for unavailable commands Add standard API for availability
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Indicates whether or not a command is currently available. When not available, provides
|
||||
* a reason.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class Availability {
|
||||
|
||||
private final String reason;
|
||||
|
||||
private Availability(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public static Availability available() {
|
||||
return new Availability(null);
|
||||
}
|
||||
|
||||
public static Availability unavailable(String reason) {
|
||||
Assert.notNull(reason, "Reason for not being available must be provided");
|
||||
return new Availability(reason);
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return reason == null;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
/**
|
||||
* Indicates that a command exists but is currently not invokable.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class CommandNotCurrentlyAvailable extends RuntimeException {
|
||||
|
||||
private final String command;
|
||||
private final Availability availability;
|
||||
|
||||
public CommandNotCurrentlyAvailable(String command, Availability availability) {
|
||||
super(String.format("Command '%s' exists but is not currently available because %s", command, availability.getReason()));
|
||||
this.command = command;
|
||||
this.availability = availability;
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
public Availability getAvailability() {
|
||||
return availability;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ 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.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
@@ -36,7 +37,16 @@ public class MethodTarget {
|
||||
|
||||
private final String help;
|
||||
|
||||
/**
|
||||
* 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, help, null);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help, Supplier<Availability> availabilityIndicator) {
|
||||
Assert.notNull(method, "Method cannot be null");
|
||||
Assert.notNull(bean, "Bean cannot be null");
|
||||
Assert.hasText(help, String.format("Help cannot be blank when trying to define command based on '%s'", method));
|
||||
@@ -44,6 +54,7 @@ public class MethodTarget {
|
||||
this.method = method;
|
||||
this.bean = bean;
|
||||
this.help = help;
|
||||
this.availabilityIndicator = availabilityIndicator != null ? availabilityIndicator : () -> Availability.available();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,13 +62,21 @@ public class MethodTarget {
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, String 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, String 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);
|
||||
return new MethodTarget(found.iterator().next(), bean, help, availabilityIndicator);
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
@@ -72,6 +91,10 @@ public class MethodTarget {
|
||||
return help;
|
||||
}
|
||||
|
||||
public Availability getAvailability() {
|
||||
return availabilityIndicator.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -131,16 +131,21 @@ public class Shell implements CommandRegistry {
|
||||
Object result;
|
||||
if (command != null) {
|
||||
MethodTarget methodTarget = methodTargets.get(command);
|
||||
List<String> wordsForArgs = wordsForArguments(command, words);
|
||||
Method method = methodTarget.getMethod();
|
||||
Availability availability = methodTarget.getAvailability();
|
||||
if (availability.isAvailable()) {
|
||||
List<String> wordsForArgs = wordsForArguments(command, words);
|
||||
Method method = methodTarget.getMethod();
|
||||
|
||||
try {
|
||||
Object[] args = resolveArgs(method, wordsForArgs);
|
||||
validateArgs(args, methodTarget);
|
||||
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
||||
}
|
||||
catch (Exception e) {
|
||||
result = e;
|
||||
try {
|
||||
Object[] args = resolveArgs(method, wordsForArgs);
|
||||
validateArgs(args, methodTarget);
|
||||
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
||||
}
|
||||
catch (Exception e) {
|
||||
result = e;
|
||||
}
|
||||
} else {
|
||||
result = new CommandNotCurrentlyAvailable(command, availability);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
==== By Name _vs._ Positional Parameters
|
||||
==== Quotes Handling
|
||||
==== Interacting with the Shell
|
||||
Line Continuation, TAB Completion, Search, etc.
|
||||
Line Continuation, kbd:[TAB] Completion, Search, etc.
|
||||
|
||||
=== Dynamic Command Availability
|
||||
|
||||
=== Built-In Commands
|
||||
* clear
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.shell.samples.legacy;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.shell.core.CommandMarker;
|
||||
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
|
||||
import org.springframework.shell.core.annotation.CliCommand;
|
||||
import org.springframework.shell.core.annotation.CliOption;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -35,6 +36,13 @@ public class LegacyCommands implements CommandMarker {
|
||||
public static final Method REGISTER_METHOD = ReflectionUtils.findMethod(LegacyCommands.class, "register", String.class, ArtifactType.class, String.class, boolean.class);
|
||||
public static final Method SUM_METHOD = ReflectionUtils.findMethod(LegacyCommands.class, "sum", int.class, int.class);
|
||||
|
||||
private boolean available = true;
|
||||
|
||||
@CliAvailabilityIndicator("register module")
|
||||
public boolean registerAvailable() {
|
||||
return available;
|
||||
}
|
||||
|
||||
@CliCommand(value = "register module", help = "Register a new module")
|
||||
public String register(
|
||||
@CliOption(mandatory = true,
|
||||
@@ -57,10 +65,11 @@ public class LegacyCommands implements CommandMarker {
|
||||
return String.format(("Successfully registered module '%s:%s'"), type, name);
|
||||
}
|
||||
|
||||
@CliCommand(value = "sum", help = "adds two numbers")
|
||||
@CliCommand(value = "sum", help = "adds two numbers. Will also toggle the 'register module' command availability")
|
||||
public int sum(
|
||||
@CliOption(key = "v1", unspecifiedDefaultValue = "38") int a,
|
||||
@CliOption(key = "v2", specifiedDefaultValue = "42") int b) {
|
||||
available = !available;
|
||||
return a + b;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.samples.standard;
|
||||
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellMethodAvailability;
|
||||
|
||||
/**
|
||||
* Showcases dynamic command availability.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@ShellComponent
|
||||
public class DynamicCommands {
|
||||
|
||||
private boolean connected;
|
||||
|
||||
private boolean authenticated;
|
||||
|
||||
public Availability authenticateAvailability() {
|
||||
return connected ? Availability.available() : Availability.unavailable("you are not connected");
|
||||
}
|
||||
|
||||
@ShellMethod("Authenticate with the system")
|
||||
public void authenticate(String credentials) {
|
||||
authenticated = "sesame".equals(credentials);
|
||||
}
|
||||
|
||||
@ShellMethod("Connect to the system")
|
||||
public void connect() {
|
||||
connected = true;
|
||||
}
|
||||
|
||||
@ShellMethod("Disconnect from the system")
|
||||
public void disconnect() {
|
||||
connected = false;
|
||||
}
|
||||
|
||||
@ShellMethod("Blow Everything up")
|
||||
@ShellMethodAvailability("dangerousAvailability")
|
||||
public String blowUp() {
|
||||
return "Boom!";
|
||||
}
|
||||
|
||||
private Availability dangerousAvailability() {
|
||||
return connected && authenticated ? Availability.available() : Availability.unavailable("you failed to authenticate. Try 'sesame'.");
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,16 @@ package org.springframework.shell.legacy;
|
||||
|
||||
import static org.springframework.util.StringUtils.collectionToDelimitedString;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.core.CommandMarker;
|
||||
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
|
||||
import org.springframework.shell.core.annotation.CliCommand;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.MethodTargetRegistrar;
|
||||
@@ -56,7 +58,8 @@ public class LegacyMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
ReflectionUtils.doWithMethods(clazz, method -> {
|
||||
CliCommand cliCommand = method.getAnnotation(CliCommand.class);
|
||||
for (String key : cliCommand.value()) {
|
||||
MethodTarget target = new MethodTarget(method, bean, cliCommand.help());
|
||||
Supplier<Availability> availabilityIndicator = bridgeAvailabilityIndicator(key, bean);
|
||||
MethodTarget target = new MethodTarget(method, bean, cliCommand.help(), availabilityIndicator);
|
||||
registry.register(key, target);
|
||||
commands.put(key, target);
|
||||
}
|
||||
@@ -64,6 +67,28 @@ public class LegacyMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
}
|
||||
}
|
||||
|
||||
private Supplier<Availability> bridgeAvailabilityIndicator(String commandKey, Object bean) {
|
||||
Class<?> clazz = bean.getClass();
|
||||
Set<Method> candidates = new HashSet<>();
|
||||
|
||||
ReflectionUtils.doWithMethods(clazz, candidates::add,
|
||||
method -> method.getAnnotation(CliAvailabilityIndicator.class) != null
|
||||
&& Arrays.asList(method.getAnnotation(CliAvailabilityIndicator.class).value()).contains(commandKey));
|
||||
|
||||
switch (candidates.size()) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return () -> {
|
||||
boolean available = (Boolean) ReflectionUtils.invokeMethod(candidates.iterator().next(), bean);
|
||||
return available ? Availability.available() : Availability.unavailable("[Unknown reason]");
|
||||
};
|
||||
default:
|
||||
throw new IllegalStateException("Looks like there are several @" + CliAvailabilityIndicator.class.getSimpleName()
|
||||
+ " for '" + commandKey + "'. Found " + candidates);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + " contributing "
|
||||
|
||||
@@ -56,7 +56,7 @@ public class LegacyMethodTargetRegistrarTest {
|
||||
|
||||
assertThat(targets).contains(entry(
|
||||
"register module",
|
||||
new MethodTarget(LegacyCommands.REGISTER_METHOD, legacyCommands, "Register a new module" )
|
||||
MethodTarget.of("register", legacyCommands, "Register a new module")
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -31,15 +31,11 @@ import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.*;
|
||||
import org.springframework.shell.standard.CommandValueProvider;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
import org.springframework.shell.Utils;
|
||||
|
||||
/**
|
||||
* A command to display help about all available commands.
|
||||
@@ -204,6 +200,14 @@ public class Help {
|
||||
}
|
||||
}
|
||||
|
||||
Availability availability = methodTarget.getAvailability();
|
||||
if (!availability.isAvailable()) {
|
||||
result.append("CURRENTLY UNAVAILABLE", AttributedStyle.BOLD).append("\n");
|
||||
result.append('\t').append("This command is currently not available because ")
|
||||
.append(availability.getReason())
|
||||
.append(".\n");
|
||||
}
|
||||
|
||||
result.append("\n");
|
||||
return result;
|
||||
}
|
||||
@@ -223,12 +227,18 @@ public class Help {
|
||||
|
||||
groupedByMethodTarget.entrySet().stream()
|
||||
.sorted(sortByFirstElement())
|
||||
.forEach(e -> result.append("\t")
|
||||
.forEach(e -> result.append(isAvailable(e) ? " " : " * ")
|
||||
.append(e.getValue().stream().collect(Collectors.joining(", ")), AttributedStyle.BOLD)
|
||||
.append(": ")
|
||||
.append(e.getKey())
|
||||
.append('\n')
|
||||
);
|
||||
|
||||
groupedByMethodTarget.entrySet().stream()
|
||||
.filter(e -> !isAvailable(e))
|
||||
.findAny()
|
||||
.ifPresent(e -> result.append("\nCommands marked with (*) are currently unavailable.\nType `help <command>` to learn more.\n"));
|
||||
|
||||
return result.append("\n");
|
||||
}
|
||||
|
||||
@@ -236,6 +246,11 @@ public class Help {
|
||||
return Comparator.comparing(e -> e.getValue().iterator().next());
|
||||
}
|
||||
|
||||
private boolean isAvailable(Map.Entry<String, Set<String>> entry) {
|
||||
String commandName = entry.getValue().iterator().next();
|
||||
return commandRegistry.listCommands().get(commandName).getAvailability().isAvailable();
|
||||
}
|
||||
|
||||
private void appendUnderlinedFormal(AttributedStringBuilder result, ParameterDescription description) {
|
||||
for (char c : description.formal().toCharArray()) {
|
||||
if (c != ' ') {
|
||||
|
||||
@@ -96,18 +96,15 @@ public class HelpTest {
|
||||
public CommandRegistry shell() {
|
||||
return () -> {
|
||||
Map<String, MethodTarget> result = new HashMap<>();
|
||||
Method method = ReflectionUtils.findMethod(Commands.class, "firstCommand", boolean.class, boolean.class, int.class, float[].class);
|
||||
MethodTarget methodTarget = new MethodTarget(method, commands(), "A rather extensive description of some command.");
|
||||
MethodTarget methodTarget = MethodTarget.of("firstCommand", commands(), "A rather extensive description of some command.");
|
||||
result.put("first-command", methodTarget);
|
||||
result.put("1st-command", methodTarget);
|
||||
|
||||
method = ReflectionUtils.findMethod(Commands.class, "secondCommand");
|
||||
methodTarget = new MethodTarget(method, commands(), "The second command. This one is known under several aliases as well.");
|
||||
methodTarget = MethodTarget.of("secondCommand", commands(), "The second command. This one is known under several aliases as well.");
|
||||
result.put("second-command", methodTarget);
|
||||
result.put("yet-another-command", methodTarget);
|
||||
|
||||
method = ReflectionUtils.findMethod(Commands.class, "thirdCommand");
|
||||
methodTarget = new MethodTarget(method, commands(), "The last command.");
|
||||
methodTarget = MethodTarget.of("thirdCommand", commands(), "The last command.");
|
||||
result.put("third-command", methodTarget);
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
AVAILABLE COMMANDS&
|
||||
&
|
||||
1st-command, first-command: A rather extensive description of some command.&
|
||||
second-command, yet-another-command: The second command. This one is known under several aliases as well.&
|
||||
third-command: The last command.&
|
||||
1st-command, first-command: A rather extensive description of some command.&
|
||||
second-command, yet-another-command: The second command. This one is known under several aliases as well.&
|
||||
third-command: The last command.&
|
||||
&
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.standard;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Used to customize the name of the method used to indicate availability of a command.
|
||||
*
|
||||
* In the absence of this annotation, the dynamic availability of a command method named {@literal foo}
|
||||
* is discovered via method {@literal fooAvailability}.
|
||||
* <ul>
|
||||
* <li>If this annotation is added to the {@literal foo}
|
||||
* method, then its {@link #value()} should be the name of an availability method (in place of
|
||||
* {@literal fooAvailability()}) that returns {@link org.springframework.shell.Availability}.</li>
|
||||
* <li>If placed on a method that returns {@link org.springframework.shell.Availability} and takes no argument,
|
||||
* then the {@link #value()} of this annotation should be the <em>command names</em> (or aliases) of the
|
||||
* commands this availability indicator is for. The special value of {@literal "*"} (the default) matches
|
||||
* all commands implemented in the current class.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD})
|
||||
@Documented
|
||||
public @interface ShellMethodAvailability {
|
||||
|
||||
/**
|
||||
* @return the name of the availability method for this command method, or if placed on an availability method, the names of
|
||||
* the commands it is for.
|
||||
*/
|
||||
String[] value() default "*";
|
||||
}
|
||||
@@ -18,20 +18,22 @@ package org.springframework.shell.standard;
|
||||
|
||||
import static org.springframework.util.StringUtils.collectionToDelimitedString;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.MethodTargetRegistrar;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.*;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* The standard implementation of {@link MethodTargetRegistrar} for new shell applications,
|
||||
* resolves methods annotated with {@link ShellMethod} on {@link ShellComponent} beans.
|
||||
* The standard implementation of {@link MethodTargetRegistrar} for new shell
|
||||
* applications, resolves methods annotated with {@link ShellMethod} on
|
||||
* {@link ShellComponent} beans.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
@@ -39,11 +41,15 @@ import org.springframework.util.ReflectionUtils;
|
||||
*/
|
||||
public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private Map<String, MethodTarget> commands = new HashMap<>();
|
||||
|
||||
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ConfigurableCommandRegistry registry) {
|
||||
Map<String, Object> commandBeans = applicationContext.getBeansWithAnnotation(ShellComponent.class);
|
||||
@@ -53,10 +59,11 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
ShellMethod shellMapping = method.getAnnotation(ShellMethod.class);
|
||||
String[] keys = shellMapping.key();
|
||||
if (keys.length == 0) {
|
||||
keys = new String[] {Utils.unCamelify(method.getName())};
|
||||
keys = new String[] { Utils.unCamelify(method.getName()) };
|
||||
}
|
||||
for (String key : keys) {
|
||||
MethodTarget target = new MethodTarget(method, bean, shellMapping.value());
|
||||
Supplier<Availability> availabilityIndicator = findAvailabilityIndicator(keys, bean, method);
|
||||
MethodTarget target = new MethodTarget(method, bean, shellMapping.value(), availabilityIndicator);
|
||||
registry.register(key, target);
|
||||
commands.put(key, target);
|
||||
}
|
||||
@@ -64,9 +71,90 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to locate an availability indicator (a no-arg method that returns
|
||||
* {@link Availability}) for the given command method. The following are tried in order
|
||||
* for method {@literal m}:
|
||||
* <ol>
|
||||
* <li>If {@literal m} bears the {@literal @}{@link ShellMethodAvailability} annotation,
|
||||
* its value should be the method name to look up</li>
|
||||
* <li>a method named {@literal "<m>Availability"} is looked up.</li>
|
||||
* <li>otherwise, if some method {@literal ai} that returns {@link Availability} and takes
|
||||
* no argument exists, that is annotated with {@literal @}{@link ShellMethodAvailability}
|
||||
* and whose annotation value contains one of the {@literal commandKeys}, then it is
|
||||
* selected</li>
|
||||
* </ol>
|
||||
*/
|
||||
private Supplier<Availability> findAvailabilityIndicator(String[] commandKeys, Object bean, Method method) {
|
||||
ShellMethodAvailability explicit = method.getAnnotation(ShellMethodAvailability.class);
|
||||
final Method indicator;
|
||||
if (explicit != null) {
|
||||
Assert.isTrue(explicit.value().length == 1, "When set on a @" +
|
||||
ShellMethod.class.getSimpleName() + " method, the value of the @"
|
||||
+ ShellMethodAvailability.class.getSimpleName() +
|
||||
" should be a single element, the name of a method that returns "
|
||||
+ Availability.class.getSimpleName() +
|
||||
". Found " + Arrays.asList(explicit.value()) + " for " + method);
|
||||
indicator = ReflectionUtils.findMethod(bean.getClass(), explicit.value()[0]);
|
||||
} // Try "<method>Availability"
|
||||
else {
|
||||
Method implicit = ReflectionUtils.findMethod(bean.getClass(), method.getName() + "Availability");
|
||||
if (implicit != null) {
|
||||
indicator = implicit;
|
||||
} else {
|
||||
Map<Method, Collection<String>> candidates = new HashMap<>();
|
||||
ReflectionUtils.doWithMethods(bean.getClass(), candidate -> {
|
||||
List<String> matchKeys = new ArrayList<>(Arrays.asList(candidate.getAnnotation(ShellMethodAvailability.class).value()));
|
||||
if (matchKeys.contains("*")) {
|
||||
Assert.isTrue(matchKeys.size() == 1, "When using '*' as a wildcard for " +
|
||||
ShellMethodAvailability.class.getSimpleName() + ", this can be the only value. Found " +
|
||||
matchKeys + " on method " + candidate);
|
||||
candidates.put(candidate, matchKeys);
|
||||
} else {
|
||||
matchKeys.retainAll(Arrays.asList(commandKeys));
|
||||
if (!matchKeys.isEmpty()) {
|
||||
candidates.put(candidate, matchKeys);
|
||||
}
|
||||
}
|
||||
}, m -> m.getAnnotation(ShellMethodAvailability.class) != null && m.getAnnotation(ShellMethod.class) == null);
|
||||
|
||||
// Make sure wildcard approach has less precedence than explicit name
|
||||
Set<Method> notUsingWildcard = candidates.entrySet().stream()
|
||||
.filter(e -> !e.getValue().contains("*"))
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Assert.isTrue(notUsingWildcard.size() <= 1,
|
||||
"Found several @" + ShellMethodAvailability.class.getSimpleName() +
|
||||
" annotated methods that could apply for " + method + ". Offending candidates are "
|
||||
+ notUsingWildcard);
|
||||
|
||||
if (notUsingWildcard.size() == 1) {
|
||||
indicator = notUsingWildcard.iterator().next();
|
||||
} // Wildcard was available
|
||||
else if (candidates.size() == 1) {
|
||||
indicator = candidates.keySet().iterator().next();
|
||||
} else {
|
||||
indicator = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (indicator != null) {
|
||||
Assert.isTrue(indicator.getReturnType().equals(Availability.class),
|
||||
"Method " + indicator + " should return " + Availability.class.getSimpleName());
|
||||
Assert.isTrue(indicator.getParameterCount() == 0, "Method " + indicator + " should be a no-arg method");
|
||||
ReflectionUtils.makeAccessible(indicator);
|
||||
return () -> (Availability) ReflectionUtils.invokeMethod(indicator, bean);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + " contributing "
|
||||
+ collectionToDelimitedString(commands.keySet(), ", ", "[", "]");
|
||||
+ collectionToDelimitedString(commands.keySet(), ", ", "[", "]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.standard;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.hamcrest.Matchers.hasEntry;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StandardMethodTargetRegistrar}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class StandardMethodTargetRegistrarTest {
|
||||
|
||||
private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
|
||||
private ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void testRegistrations() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Sample.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
|
||||
MethodTarget methodTarget = registry.listCommands().get("say-hello");
|
||||
assertThat(methodTarget, notNullValue());
|
||||
assertThat(methodTarget.getHelp(), is("some command"));
|
||||
assertThat(methodTarget.getMethod(), is(ReflectionUtils.findMethod(Sample.class, "sayHello", String.class)));
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(true));
|
||||
|
||||
methodTarget = registry.listCommands().get("hi");
|
||||
assertThat(methodTarget, notNullValue());
|
||||
assertThat(methodTarget.getHelp(), is("method with alias"));
|
||||
assertThat(methodTarget.getMethod(), is(ReflectionUtils.findMethod(Sample.class, "greet", String.class)));
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(true));
|
||||
methodTarget = registry.listCommands().get("alias");
|
||||
assertThat(methodTarget, notNullValue());
|
||||
assertThat(methodTarget.getHelp(), is("method with alias"));
|
||||
assertThat(methodTarget.getMethod(), is(ReflectionUtils.findMethod(Sample.class, "greet", String.class)));
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(true));
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class Sample {
|
||||
|
||||
@ShellMethod("some command")
|
||||
public String sayHello(String what) {
|
||||
return "hello " + what;
|
||||
}
|
||||
|
||||
@ShellMethod(value = "method with alias", key = {"hi", "alias"})
|
||||
public String greet(String what) {
|
||||
return "hi " + what;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicators() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class);
|
||||
|
||||
MethodTarget methodTarget = registry.listCommands().get("say-hello");
|
||||
assertThat(methodTarget.getMethod(), is(ReflectionUtils.findMethod(SampleWithAvailability.class, "sayHello")));
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(true));
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(false));
|
||||
assertThat(methodTarget.getAvailability().getReason(), is("sayHelloAvailability"));
|
||||
sample.available = true;
|
||||
|
||||
methodTarget = registry.listCommands().get("hi");
|
||||
assertThat(methodTarget.getMethod(), is(ReflectionUtils.findMethod(SampleWithAvailability.class, "hi")));
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(true));
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(false));
|
||||
assertThat(methodTarget.getAvailability().getReason(), is("customAvailabilityMethod"));
|
||||
sample.available = true;
|
||||
|
||||
methodTarget = registry.listCommands().get("bonjour");
|
||||
assertThat(methodTarget.getMethod(), is(ReflectionUtils.findMethod(SampleWithAvailability.class, "bonjour")));
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(true));
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable(), is(false));
|
||||
assertThat(methodTarget.getAvailability().getReason(), is("availabilityForSeveralCommands"));
|
||||
sample.available = true;
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class SampleWithAvailability {
|
||||
|
||||
private boolean available = true;
|
||||
|
||||
@ShellMethod("some command with an implicit availability indicator")
|
||||
public void sayHello() {
|
||||
|
||||
}
|
||||
public Availability sayHelloAvailability() {
|
||||
return available ? Availability.available() : Availability.unavailable("sayHelloAvailability");
|
||||
}
|
||||
|
||||
|
||||
@ShellMethodAvailability("customAvailabilityMethod")
|
||||
@ShellMethod("some method with an explicit availability indicator")
|
||||
public void hi() {
|
||||
|
||||
}
|
||||
public Availability customAvailabilityMethod() {
|
||||
return available ? Availability.available() : Availability.unavailable("customAvailabilityMethod");
|
||||
}
|
||||
|
||||
@ShellMethod(value = "some method with an explicit availability indicator", key = {"bonjour", "salut"})
|
||||
public void bonjour() {
|
||||
|
||||
}
|
||||
@ShellMethodAvailability({"salut", "other"})
|
||||
public Availability availabilityForSeveralCommands() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityForSeveralCommands");
|
||||
}
|
||||
|
||||
|
||||
@ShellMethod("a command whose availability indicator will come from wildcard")
|
||||
public void wild() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethodAvailability("*")
|
||||
private Availability availabilityFromWildcard() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityFromWildcard");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorErrorMultipleExplicit() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.expectMessage("When set on a @ShellMethod method, the value of the @ShellMethodAvailability should be a single element");
|
||||
thrown.expectMessage("Found [one, two]");
|
||||
thrown.expectMessage("wrong()");
|
||||
|
||||
registrar.register(registry);
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorOnShellMethod {
|
||||
|
||||
@ShellMethodAvailability({"one", "two"})
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorWildcardNotAlone() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.expectMessage("When using '*' as a wildcard for ShellMethodAvailability, this can be the only value. Found [one, *]");
|
||||
thrown.expectMessage("availability()");
|
||||
|
||||
registrar.register(registry);
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorWildcardNotAlone {
|
||||
|
||||
@ShellMethodAvailability({"one", "*"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorAmbiguous() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.expectMessage("Found several @ShellMethodAvailability");
|
||||
thrown.expectMessage("wrong()");
|
||||
thrown.expectMessage("availability()");
|
||||
thrown.expectMessage("otherAvailability()");
|
||||
|
||||
registrar.register(registry);
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorAmbiguous {
|
||||
|
||||
@ShellMethodAvailability({"one", "wrong"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethodAvailability({"bar", "wrong"})
|
||||
public Availability otherAvailability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user