Rework command subsystem
- Focus of these changes are to introduce a new command system based on real registrations (new way) instead of continuously (old way) resolve methods and its parameters via reflection. - There's a lot of changes as this resolution via reflection had its hooks almost everywhere and thus most changes are just refactorings. - Order to understand real changes I'd start to look classes under `org.springframework.shell.command` package as it defines new registration, catalog and parser classes. Also samples contain new classes to demonstrate new functionality. - Fixes #380
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,14 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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;
|
||||
import java.util.Map;
|
||||
@@ -28,44 +26,45 @@ import java.util.Optional;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
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.CommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
import org.springframework.shell.standard.StandardParameterResolver;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for the {@link Help} command.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = HelpTest.Config.class)
|
||||
public class HelpTest {
|
||||
@ContextConfiguration(classes = HelpTests.Config.class)
|
||||
public class HelpTests {
|
||||
|
||||
private static Locale previousLocale;
|
||||
private String testName;
|
||||
private Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
private CommandsPojo commandsPojo = new CommandsPojo();
|
||||
|
||||
@MockBean
|
||||
private CommandCatalog commandCatalog;
|
||||
|
||||
@Autowired
|
||||
private Help help;
|
||||
|
||||
@BeforeAll
|
||||
public static void setAssumedLocale() {
|
||||
@@ -80,25 +79,106 @@ public class HelpTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup(TestInfo testInfo) {
|
||||
registrations.clear();
|
||||
Optional<Method> testMethod = testInfo.getTestMethod();
|
||||
if (testMethod.isPresent()) {
|
||||
this.testName = testMethod.get().getName();
|
||||
}
|
||||
Mockito.when(commandCatalog.getRegistrations()).thenReturn(registrations);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private Help help;
|
||||
|
||||
@Test
|
||||
public void testCommandHelp() throws Exception {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("first-command")
|
||||
.help("A rather extensive description of some command.")
|
||||
.withTarget()
|
||||
.method(commandsPojo, "firstCommand")
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('r')
|
||||
.description("Whether to delete recursively")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('f')
|
||||
.description("Do not ask for confirmation. YOLO")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('n')
|
||||
.description("The answer to everything")
|
||||
.defaultValue("42")
|
||||
.type(int.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('o')
|
||||
.description("Some other parameters")
|
||||
.type(float[].class)
|
||||
.and()
|
||||
.build();
|
||||
registrations.put("first-command", registration);
|
||||
registrations.put("1st-command", registration);
|
||||
CharSequence help = this.help.help("first-command").toString();
|
||||
Assertions.assertThat(help).isEqualTo(sample());
|
||||
assertThat(help).isEqualTo(sample());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandList() throws Exception {
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("first-command")
|
||||
.help("A rather extensive description of some command.")
|
||||
.withTarget()
|
||||
.method(commandsPojo, "firstCommand")
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('r')
|
||||
.and()
|
||||
.build();
|
||||
registrations.put("first-command", registration1);
|
||||
registrations.put("1st-command", registration1);
|
||||
|
||||
CommandRegistration registration2 = CommandRegistration.builder()
|
||||
.command("second-command")
|
||||
.help("The second command. This one is known under several aliases as well.")
|
||||
.withTarget()
|
||||
.method(commandsPojo, "secondCommand")
|
||||
.and()
|
||||
.build();
|
||||
registrations.put("second-command", registration2);
|
||||
registrations.put("yet-another-command", registration2);
|
||||
|
||||
CommandRegistration registration3 = CommandRegistration.builder()
|
||||
.command("second-command")
|
||||
.help("The last command.")
|
||||
.withTarget()
|
||||
.method(commandsPojo, "thirdCommand")
|
||||
.and()
|
||||
.build();
|
||||
registrations.put("third-command", registration3);
|
||||
|
||||
CommandRegistration registration4 = CommandRegistration.builder()
|
||||
.command("first-group-command")
|
||||
.help("The first command in a separate group.")
|
||||
.group("Example Group")
|
||||
.withTarget()
|
||||
.method(commandsPojo, "firstCommandInGroup")
|
||||
.and()
|
||||
.build();
|
||||
registrations.put("first-group-command", registration4);
|
||||
|
||||
CommandRegistration registration5 = CommandRegistration.builder()
|
||||
.command("second-group-command")
|
||||
.help("The second command in a separate group.")
|
||||
.group("Example Group")
|
||||
.withTarget()
|
||||
.method(commandsPojo, "secondCommandInGroup")
|
||||
.and()
|
||||
.build();
|
||||
registrations.put("second-group-command", registration5);
|
||||
|
||||
String list = this.help.help(null).toString();
|
||||
Assertions.assertThat(list).isEqualTo(sample());
|
||||
assertThat(list).isEqualTo(sample());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,7 +189,7 @@ public class HelpTest {
|
||||
}
|
||||
|
||||
private String sample() throws IOException {
|
||||
InputStream is = new ClassPathResource(HelpTest.class.getSimpleName() + "-" + testName + ".txt", HelpTest.class).getInputStream();
|
||||
InputStream is = new ClassPathResource(HelpTests.class.getSimpleName() + "-" + testName + ".txt", HelpTests.class).getInputStream();
|
||||
return FileCopyUtils.copyToString(new InputStreamReader(is, "UTF-8")).replace("&", "");
|
||||
}
|
||||
|
||||
@@ -117,62 +197,18 @@ public class HelpTest {
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public Help help(CommandRegistry commandRegistry) {
|
||||
public Help help() {
|
||||
return new Help();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandRegistry shell() {
|
||||
|
||||
return new CommandRegistry() {
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> listCommands() {
|
||||
Map<String, MethodTarget> result = new HashMap<>();
|
||||
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(), 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(), new Command.Help("The last command."));
|
||||
result.put("third-command", methodTarget);
|
||||
|
||||
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(), new Command.Help("The second command in a separate group.", "Example Group"));
|
||||
result.put("second-group-command", methodTarget);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCommand(String name, MethodTarget target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCommand(String name) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ParameterResolver parameterResolver() {
|
||||
return new StandardParameterResolver(new DefaultConversionService(), Collections.emptySet());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Object commands() {
|
||||
return new Commands();
|
||||
}
|
||||
|
||||
// @Bean
|
||||
// public ParameterResolver parameterResolver() {
|
||||
// return new StandardParameterResolver(new DefaultConversionService(), Collections.emptySet());
|
||||
// }
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
static class Commands {
|
||||
static class CommandsPojo {
|
||||
|
||||
@ShellMethod(prefix = "--")
|
||||
public void firstCommand(
|
||||
@@ -186,28 +222,22 @@ public class HelpTest {
|
||||
// Single key, arity > 1.
|
||||
@ShellOption(help = "Some other parameters", arity = 3, value = "-o") float[] o
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethod
|
||||
public void secondCommand() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethod
|
||||
public void thirdCommand() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethod
|
||||
public void firstCommandInGroup() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethod
|
||||
public void secondCommandInGroup() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,22 @@ NAME
|
||||
first-command - A rather extensive description of some command.
|
||||
|
||||
SYNOPSYS
|
||||
first-command [-r] [-f] [[-n] int] [-o] float float float
|
||||
first-command [[-r] boolean] [[-f] boolean] [[-n] int] [-o] float[]
|
||||
|
||||
OPTIONS
|
||||
-r Whether to delete recursively
|
||||
-r boolean
|
||||
Whether to delete recursively
|
||||
[Optional, default = false]
|
||||
|
||||
-f or --force
|
||||
-f boolean
|
||||
Do not ask for confirmation. YOLO
|
||||
[Optional, default = false]
|
||||
|
||||
-n int
|
||||
The answer to everything
|
||||
[Optional, default = 42]
|
||||
[must be less than or equal to 5]
|
||||
|
||||
-o float float float
|
||||
-o float[]
|
||||
Some other parameters
|
||||
[Mandatory]
|
||||
|
||||
Reference in New Issue
Block a user