Add automatic command grouping
Fixes #163 Introduce Command and Command.Help
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 java.util.Objects;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public interface Command {
|
||||
|
||||
/**
|
||||
* Encapsulates help metadata about a shell command.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
class Help {
|
||||
|
||||
/**
|
||||
* Optional group to gather related commands together.
|
||||
*/
|
||||
private final String group;
|
||||
|
||||
/**
|
||||
* A required, short one sentence description of the command. Should start with a capital and end with a dot
|
||||
* for consistency.
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
public Help(String description) {
|
||||
this(description, null);
|
||||
}
|
||||
|
||||
public Help(String description, String group) {
|
||||
Assert.isTrue(StringUtils.hasText(description), "Command description cannot be null or empty");
|
||||
this.description = description;
|
||||
this.group = StringUtils.hasText(group) ? group : "";
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Help help = (Help) o;
|
||||
return Objects.equals(group, help.group) &&
|
||||
Objects.equals(description, help.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(group, description);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,15 +29,13 @@ import org.springframework.util.ReflectionUtils;
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class MethodTarget {
|
||||
public class MethodTarget implements Command {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Object bean;
|
||||
|
||||
private final String help;
|
||||
|
||||
private final String group;
|
||||
private final Help help;
|
||||
|
||||
/**
|
||||
* If not null, returns whether or not the command is currently available. Implementations must be idempotent.
|
||||
@@ -45,22 +43,21 @@ public class MethodTarget {
|
||||
private final Supplier<Availability> availabilityIndicator;
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help) {
|
||||
this(method, bean, help, null, null);
|
||||
this(method, bean, new Help(help, null), null);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help, Supplier<Availability> availabilityIndicator) {
|
||||
this(method, bean, help, null, availabilityIndicator);
|
||||
this(method, bean, new Help(help, null), availabilityIndicator);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help, String group, Supplier<Availability> availabilityIndicator) {
|
||||
public MethodTarget(Method method, Object bean, Help 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));
|
||||
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.group = group != null ? group : "";
|
||||
this.availabilityIndicator = availabilityIndicator != null ? availabilityIndicator : () -> Availability.available();
|
||||
}
|
||||
|
||||
@@ -68,30 +65,22 @@ public class MethodTarget {
|
||||
* 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) {
|
||||
return of(name, bean, help, null, null);
|
||||
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, String help, String group) {
|
||||
return of(name, bean, help, group, 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, String group, Supplier<Availability> availabilityIndicator) {
|
||||
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, group, availabilityIndicator);
|
||||
return new MethodTarget(found.iterator().next(), bean, help, availabilityIndicator);
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
@@ -103,11 +92,11 @@ public class MethodTarget {
|
||||
}
|
||||
|
||||
public String getHelp() {
|
||||
return help;
|
||||
return help.getDescription();
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
return help.getGroup();
|
||||
}
|
||||
|
||||
public Availability getAvailability() {
|
||||
@@ -123,7 +112,7 @@ public class MethodTarget {
|
||||
|
||||
if (!method.equals(that.method)) return false;
|
||||
if (!bean.equals(that.bean)) return false;
|
||||
if (!group.equals(that.group)) return false;
|
||||
if (!help.equals(that.help)) return false;
|
||||
return help.equals(that.help);
|
||||
|
||||
}
|
||||
@@ -133,7 +122,7 @@ public class MethodTarget {
|
||||
int result = method.hashCode();
|
||||
result = 31 * result + bean.hashCode();
|
||||
result = 31 * result + help.hashCode();
|
||||
result = 31 * result + group.hashCode();
|
||||
result = 31 * result + help.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import static org.hamcrest.collection.IsMapContaining.hasEntry;
|
||||
import static org.hamcrest.collection.IsMapContaining.hasKey;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.hamcrest.collection.IsMapContaining;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
@@ -38,7 +37,7 @@ public class ConfigurableCommandRegistryTest {
|
||||
@Test
|
||||
public void testRegistration() {
|
||||
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry();
|
||||
registry.register("foo", MethodTarget.of("toString", this, "some command"));
|
||||
registry.register("foo", MethodTarget.of("toString", this, new Command.Help("some command")));
|
||||
|
||||
assertThat(registry.listCommands(), hasKey("foo"));
|
||||
}
|
||||
@@ -46,14 +45,14 @@ public class ConfigurableCommandRegistryTest {
|
||||
@Test
|
||||
public void testDoubleRegistration() {
|
||||
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry();
|
||||
registry.register("foo", MethodTarget.of("toString", this, "some command"));
|
||||
registry.register("foo", MethodTarget.of("toString", this, new Command.Help("some command")));
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
thrown.expectMessage("foo");
|
||||
thrown.expectMessage("toString");
|
||||
thrown.expectMessage("hashCode");
|
||||
|
||||
registry.register("foo", MethodTarget.of("hashCode", this, "some command"));
|
||||
registry.register("foo", MethodTarget.of("hashCode", this, new Command.Help("some command")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public class ShellTest {
|
||||
when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any());
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, new Command.Help("Say hello")));
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -101,7 +101,7 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?", null);
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(isA(CommandNotFound.class));
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("bonjour", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
shell.methodTargets = Collections.singletonMap("bonjour", MethodTarget.of("helloWorld", this, new Command.Help("Say hello")));
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -118,7 +118,7 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "helloworld how are you doing ?", null);
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(isA(CommandNotFound.class));
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("hello", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
shell.methodTargets = Collections.singletonMap("hello", MethodTarget.of("helloWorld", this, new Command.Help("Say hello")));
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -137,7 +137,7 @@ public class ShellTest {
|
||||
when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(any());
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, "Say hello"));
|
||||
shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, new Command.Help("Say hello")));
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -156,7 +156,7 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "fail", null);
|
||||
doThrow(new Exit()).when(resultHandler).handleResult(isA(SomeException.class));
|
||||
|
||||
shell.methodTargets = Collections.singletonMap("fail", MethodTarget.of("failing", this, "Will throw an exception"));
|
||||
shell.methodTargets = Collections.singletonMap("fail", MethodTarget.of("failing", this, new Command.Help("Will throw an exception")));
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -185,7 +185,7 @@ public class ShellTest {
|
||||
shell.applicationContext = mock(ApplicationContext.class);
|
||||
when(shell.applicationContext.getBeansOfType(MethodTargetRegistrar.class))
|
||||
.thenReturn(Collections.singletonMap("foo", r -> {
|
||||
r.register("hw", MethodTarget.of("helloWorld", this, "hellow world"));
|
||||
r.register("hw", MethodTarget.of("helloWorld", this, new Command.Help("hellow world")));
|
||||
}));
|
||||
|
||||
thrown.expect(ParameterResolverMissingException.class);
|
||||
@@ -198,8 +198,8 @@ public class ShellTest {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(shell.applicationContext.getBeansOfType(MethodTargetRegistrar.class))
|
||||
.thenReturn(Collections.singletonMap("foo", r -> {
|
||||
r.register("hello world", MethodTarget.of("helloWorld", this, "hellow world"));
|
||||
r.register("another command", MethodTarget.of("helloWorld", this, "another command"));
|
||||
r.register("hello world", MethodTarget.of("helloWorld", this, new Command.Help("hellow world")));
|
||||
r.register("another command", MethodTarget.of("helloWorld", this, new Command.Help("another command")));
|
||||
}));
|
||||
shell.gatherMethodTargets();
|
||||
|
||||
|
||||
@@ -596,6 +596,57 @@ But it's often good practice to put related commands in the same class, and the
|
||||
can benefit from that.
|
||||
====
|
||||
|
||||
[[organizing-commands]]
|
||||
=== Organizing Commands
|
||||
When your shell starts to provide a lot of functionality, you may en up
|
||||
with a lot of commands, which could be confusing for your users. Typing `help`
|
||||
they would see a daunting list of commands, organized by alphabetical order,
|
||||
which may not always make sense.
|
||||
|
||||
To alleviate this, Spring Shell provides the ability to group commands together,
|
||||
with reasonable defaults. Related commands would then end up in the same _group_ (_e.g._ `User Management Commands`)
|
||||
and be displayed together in the help screen and other places.
|
||||
|
||||
By default, commands will be grouped according to the class they are implemented in,
|
||||
turning the camel case class name into separate words (so `URLRelatedCommands` becomes `URL Related Commands`).
|
||||
This is a very sensible default, as related commands are often already in the class anyway,
|
||||
for they need to use the same collaborating objects.
|
||||
|
||||
If however, this behavior does not suit you, you can override the group for a
|
||||
command in the following ways, in order of priority:
|
||||
|
||||
* specifying a `group()` in the `@ShellMethod` annotation
|
||||
* placing a `@ShellCommandGroup` on the class the command is defined in. This will apply
|
||||
the group for all commands defined in that class (unless overridden as above)
|
||||
* placing a `@ShellCommandGroup` on the package (_via_ `package-info.java`)
|
||||
the command is defined in. This will apply to all commands defined in the
|
||||
package (unless overridden at the method or class level as explained above)
|
||||
|
||||
Here is a short example:
|
||||
[source,java]
|
||||
----
|
||||
public class UserCommands {
|
||||
@ShellCommand(value = "This command ends up in the 'User Commands' group")
|
||||
public void foo() {}
|
||||
|
||||
@ShellCommand(value = "This command ends up in the 'Other Commands' group",
|
||||
group = "Other Commands")
|
||||
public void bar() {}
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
@ShellCommandGroup("Other Commands")
|
||||
public class SomeCommands {
|
||||
@ShellMethod(value = "This one is in 'Other Commands'")
|
||||
public void wizz() {}
|
||||
|
||||
@ShellMethod(value = "And this one is 'Yet Another Group'",
|
||||
group = "Yet Another Group")
|
||||
public void last() {}
|
||||
}
|
||||
----
|
||||
|
||||
[[built-in-commands]]
|
||||
=== Built-In Commands
|
||||
Any application built using the `{starter-artifactId}` artifact
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell.Command;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.MethodTargetRegistrar;
|
||||
@@ -56,7 +57,7 @@ public class LegacyMethodTargetRegistrarTest {
|
||||
|
||||
assertThat(targets).contains(entry(
|
||||
"register module",
|
||||
MethodTarget.of("register", legacyCommands, "Register a new module")
|
||||
MethodTarget.of("register", legacyCommands, new Command.Help("Register a new module"))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,4 +19,7 @@
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@ShellCommandGroup("Built-In Commands")
|
||||
package org.springframework.shell.standard.commands;
|
||||
|
||||
import org.springframework.shell.standard.ShellCommandGroup;
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.springframework.shell.standard.commands;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
@@ -38,6 +37,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.shell.Command;
|
||||
import org.springframework.shell.standard.StandardParameterResolver;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
@@ -48,7 +48,6 @@ import org.springframework.shell.standard.ShellOption;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
|
||||
@@ -114,21 +113,21 @@ public class HelpTest {
|
||||
public CommandRegistry shell() {
|
||||
return () -> {
|
||||
Map<String, MethodTarget> result = new HashMap<>();
|
||||
MethodTarget methodTarget = MethodTarget.of("firstCommand", commands(), "A rather extensive description of some command.");
|
||||
MethodTarget methodTarget = MethodTarget.of("firstCommand", commands(), new Command.Help("A rather extensive description of some command."));
|
||||
result.put("first-command", methodTarget);
|
||||
result.put("1st-command", methodTarget);
|
||||
|
||||
methodTarget = MethodTarget.of("secondCommand", commands(), "The second command. This one is known under several aliases as well.");
|
||||
methodTarget = MethodTarget.of("secondCommand", commands(), new Command.Help("The second command. This one is known under several aliases as well."));
|
||||
result.put("second-command", methodTarget);
|
||||
result.put("yet-another-command", methodTarget);
|
||||
|
||||
methodTarget = MethodTarget.of("thirdCommand", commands(), "The last command.");
|
||||
methodTarget = MethodTarget.of("thirdCommand", commands(), new Command.Help("The last command."));
|
||||
result.put("third-command", methodTarget);
|
||||
|
||||
methodTarget = MethodTarget.of("firstCommandInGroup", commands(), "The first command in a separate group.", "Example Group");
|
||||
methodTarget = MethodTarget.of("firstCommandInGroup", commands(), new Command.Help("The first command in a separate group.", "Example Group"));
|
||||
result.put("first-group-command", methodTarget);
|
||||
|
||||
methodTarget = MethodTarget.of("secondCommandInGroup", commands(), "The second command in a separate group.", "Example Group");
|
||||
methodTarget = MethodTarget.of("secondCommandInGroup", commands(), new Command.Help("The second command in a separate group.", "Example Group"));
|
||||
result.put("second-group-command", methodTarget);
|
||||
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Used to indicate the default group of shell commands, either at the package or class level.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.PACKAGE, ElementType.TYPE})
|
||||
@Documented
|
||||
public @interface ShellCommandGroup {
|
||||
|
||||
/**
|
||||
* The default value for the group label, which when set<ul>
|
||||
* <li>on a class, will mean to look at the package level</li>
|
||||
* <li>on a package, to go back at the class level and infer a name from the class name.</li>
|
||||
* </ul>
|
||||
*/
|
||||
String INHERIT_AND_INFER = "";
|
||||
|
||||
/**
|
||||
* @return
|
||||
* An explicit value for the group, which will apply to all commands in the owning class or package, depending
|
||||
* on where this annotation is set.
|
||||
*/
|
||||
String value() default INHERIT_AND_INFER;
|
||||
|
||||
}
|
||||
@@ -33,6 +33,13 @@ import java.lang.annotation.Target;
|
||||
@Documented
|
||||
public @interface ShellMethod {
|
||||
|
||||
/**
|
||||
* The default value for {@link #group()}, meaning that the group will be inherited from the explicit value set
|
||||
* on the containing element (class then package) or ultimately inferred.
|
||||
* @see ShellCommandGroup
|
||||
*/
|
||||
String INHERITED = "";
|
||||
|
||||
/**
|
||||
* The name(s) by which this method can be invoked via Spring Shell. If not specified, the actual method name
|
||||
* will be used (turning camelCase humps into "-").
|
||||
@@ -55,9 +62,10 @@ public @interface ShellMethod {
|
||||
|
||||
/**
|
||||
* The command group which this command belongs to. The command group is used when printing a list of
|
||||
* commands to group related commands.
|
||||
* commands to group related commands. By default, group is first looked up from owning class then package,
|
||||
* and if not explicitly set, is inferred from class name.
|
||||
* @return name of the command group
|
||||
*/
|
||||
String group() default "";
|
||||
String group() default INHERITED;
|
||||
|
||||
}
|
||||
|
||||
@@ -21,14 +21,15 @@ import static org.springframework.util.StringUtils.collectionToDelimitedString;
|
||||
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.core.annotation.AnnotationUtils;
|
||||
import org.springframework.shell.*;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The standard implementation of {@link MethodTargetRegistrar} for new shell
|
||||
@@ -61,10 +62,10 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
if (keys.length == 0) {
|
||||
keys = new String[] { Utils.unCamelify(method.getName()) };
|
||||
}
|
||||
String group = shellMapping.group();
|
||||
String group = getOrInferGroup(method);
|
||||
for (String key : keys) {
|
||||
Supplier<Availability> availabilityIndicator = findAvailabilityIndicator(keys, bean, method);
|
||||
MethodTarget target = new MethodTarget(method, bean, shellMapping.value(), group, availabilityIndicator);
|
||||
MethodTarget target = new MethodTarget(method, bean, new Command.Help(shellMapping.value(), group), availabilityIndicator);
|
||||
registry.register(key, target);
|
||||
commands.put(key, target);
|
||||
}
|
||||
@@ -72,6 +73,32 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the group from the following places, in order:<ul>
|
||||
* <li>explicit annotation at the method level</li>
|
||||
* <li>explicit annotation at the class level</li>
|
||||
* <li>explicit annotation at the package level</li>
|
||||
* <li>implicit from the class name</li>
|
||||
* </ul>
|
||||
*/
|
||||
private String getOrInferGroup(Method method) {
|
||||
ShellMethod methodAnn = AnnotationUtils.getAnnotation(method, ShellMethod.class);
|
||||
if (!methodAnn.group().equals(ShellMethod.INHERITED)) {
|
||||
return methodAnn.group();
|
||||
}
|
||||
Class<?> clazz = method.getDeclaringClass();
|
||||
ShellCommandGroup classAnn = AnnotationUtils.getAnnotation(clazz, ShellCommandGroup.class);
|
||||
if (classAnn != null && !classAnn.value().equals(ShellCommandGroup.INHERIT_AND_INFER)) {
|
||||
return classAnn.value();
|
||||
}
|
||||
ShellCommandGroup packageAnn = AnnotationUtils.getAnnotation(clazz.getPackage(), ShellCommandGroup.class);
|
||||
if (packageAnn != null && !packageAnn.value().equals(ShellCommandGroup.INHERIT_AND_INFER)) {
|
||||
return packageAnn.value();
|
||||
}
|
||||
// Shameful copy/paste from https://stackoverflow.com/questions/7593969/regex-to-split-camelcase-or-titlecase-advanced
|
||||
return StringUtils.arrayToDelimitedString(clazz.getSimpleName().split("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])"), " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
package org.springframework.shell.standard;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
@@ -26,6 +25,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.standard.test1.GroupOneCommands;
|
||||
import org.springframework.shell.standard.test2.GroupTwoCommands;
|
||||
import org.springframework.shell.standard.test2.GroupThreeCommands;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.hamcrest.Matchers.hasEntry;
|
||||
@@ -33,6 +35,8 @@ import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StandardMethodTargetRegistrar}.
|
||||
*
|
||||
@@ -242,4 +246,20 @@ public class StandardMethodTargetRegistrarTest {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Test
|
||||
public void testGrouping() {
|
||||
ApplicationContext context = new AnnotationConfigApplicationContext(GroupOneCommands.class,
|
||||
GroupTwoCommands.class, GroupThreeCommands.class);
|
||||
registrar.setApplicationContext(context);
|
||||
registrar.register(registry);
|
||||
|
||||
Map<String, MethodTarget> commands = registry.listCommands();
|
||||
Assertions.assertThat(commands.get("explicit1").getGroup()).isEqualTo("Explicit Group Method Level 1");
|
||||
Assertions.assertThat(commands.get("explicit2").getGroup()).isEqualTo("Explicit Group Method Level 2");
|
||||
Assertions.assertThat(commands.get("explicit3").getGroup()).isEqualTo("Explicit Group Method Level 3");
|
||||
Assertions.assertThat(commands.get("implicit1").getGroup()).isEqualTo("Implicit Group Package Level 1");
|
||||
Assertions.assertThat(commands.get("implicit2").getGroup()).isEqualTo("Group Two Commands");
|
||||
Assertions.assertThat(commands.get("implicit3").getGroup()).isEqualTo("Explicit Group 3 Class Level");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.test1;
|
||||
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
|
||||
@ShellComponent
|
||||
public class GroupOneCommands {
|
||||
|
||||
@ShellMethod(value = "Do Something.", group = "Explicit Group Method Level 1")
|
||||
public void explicit1() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethod(value = "Do Something Else")
|
||||
public void implicit1() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@ShellCommandGroup("Implicit Group Package Level 1")
|
||||
package org.springframework.shell.standard.test1;
|
||||
|
||||
import org.springframework.shell.standard.ShellCommandGroup;
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.test2;
|
||||
|
||||
import org.springframework.shell.standard.ShellCommandGroup;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
|
||||
@ShellComponent
|
||||
@ShellCommandGroup("Explicit Group 3 Class Level")
|
||||
public class GroupThreeCommands {
|
||||
|
||||
@ShellMethod(value = "Do Something.", group = "Explicit Group Method Level 3")
|
||||
public void explicit3() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethod(value = "Do Something Else")
|
||||
public void implicit3() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.test2;
|
||||
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
|
||||
@ShellComponent
|
||||
public class GroupTwoCommands {
|
||||
|
||||
@ShellMethod(value = "Do Something.", group = "Explicit Group Method Level 2")
|
||||
public void explicit2() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethod(value = "Do Something Else")
|
||||
public void implicit2() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user