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,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);
}
}
}

View File

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

View File

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

View File

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