Add automatic command grouping

Fixes #163

Introduce Command and Command.Help
This commit is contained in:
Eric Bottard
2017-09-27 10:49:12 +02:00
parent 531dc35abe
commit f0e5c45ee9
16 changed files with 404 additions and 53 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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");
}
}

View File

@@ -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() {
}
}

View File

@@ -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;

View File

@@ -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() {
}
}

View File

@@ -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() {
}
}