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:
Janne Valkealahti
2022-05-06 08:32:53 +01:00
parent 81e5bf8c81
commit 8a23518b84
91 changed files with 7026 additions and 2291 deletions

View File

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

View File

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