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-2021 the original author or authors.
* Copyright 2017-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,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.samples;
import org.jline.utils.AttributedString;
@@ -30,6 +29,7 @@ import org.springframework.shell.jline.PromptProvider;
* <p>Creates the application context and start the REPL.</p>
*
* @author Eric Bottard
* @author Janne Valkealahti
*/
@SpringBootApplication
public class SpringShellSample {

View File

@@ -71,6 +71,11 @@ public class Commands {
return a + b + c;
}
@ShellMethod("Concat strings.")
public String concat(String a, String b, String c) {
return a + b + c;
}
@ShellMethod("Fails with an exception. Shows enum conversion.")
public void fail(ElementType elementType) {
throw new IllegalArgumentException("You said " + elementType);

View File

@@ -0,0 +1,90 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://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.samples.standard;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.command.CommandRegistration;
@Configuration
public class FunctionCommands {
@Bean
public CommandRegistration commandRegistration1() {
return CommandRegistration.builder()
.command("function", "command1")
.help("function sample")
.group("Function Commands")
.withTarget()
.function(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return String.format("hi, arg1 value is '%s'", arg1);
})
.and()
.withOption()
.longNames("arg1")
.and()
.build();
}
@Bean
public CommandRegistration commandRegistration2() {
return CommandRegistration.builder()
.command("function", "command2")
.help("function sample")
.group("Function Commands")
.withTarget()
.function(ctx -> {
Boolean a = ctx.getOptionValue("a");
Boolean b = ctx.getOptionValue("b");
Boolean c = ctx.getOptionValue("c");
return String.format("hi, boolean values for a, b, c are '%s' '%s' '%s'", a, b, c);
})
.and()
.withOption()
.shortNames('a')
.type(boolean.class)
.and()
.withOption()
.shortNames('b')
.type(boolean.class)
.and()
.withOption()
.shortNames('c')
.type(boolean.class)
.and()
.build();
}
@Bean
public CommandRegistration commandRegistration3() {
return CommandRegistration.builder()
.command("function", "command3")
.help("function sample")
.group("Function Commands")
.withTarget()
.consumer(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
ctx.getTerminal().writer()
.println(String.format("hi, arg1 value is '%s'", arg1));
})
.and()
.withOption()
.longNames("arg1")
.and()
.build();
}
}

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.shell.samples.standard;
import org.springframework.shell.MethodTarget;
import java.util.function.Function;
import org.springframework.shell.command.CommandContext;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.AbstractShellComponent;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@@ -24,42 +27,90 @@ import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class RegisterCommands extends AbstractShellComponent {
private final static String GROUP = "Register Commands";
private final PojoMethods pojoMethods = new PojoMethods();
private final CommandRegistration registered1;
private final CommandRegistration registered2;
private final CommandRegistration registered3;
@ShellMethod(key = "register add", value = "Register commands", group = "Register Commands")
public RegisterCommands() {
registered1 = CommandRegistration.builder()
.command("register registered1")
.group(GROUP)
.help("registered1 command")
.withTarget()
.method(pojoMethods, "registered1")
.and()
.build();
registered2 = CommandRegistration.builder()
.command("register registered2")
.help("registered2 command")
.group(GROUP)
.withTarget()
.method(pojoMethods, "registered2")
.and()
.withOption()
.longNames("arg1")
.and()
.build();
registered3 = CommandRegistration.builder()
.command("register registered3")
.help("registered3 command")
.group(GROUP)
.withTarget()
.method(pojoMethods, "registered3")
.and()
.build();
}
@ShellMethod(key = "register add", value = "Register commands", group = GROUP)
public String register() {
MethodTarget target1 = MethodTarget.of("dynamic1", pojoMethods, "Dynamic1 command", "Register Commands");
MethodTarget target2 = MethodTarget.of("dynamic2", pojoMethods, "Dynamic2 command", "Register Commands");
MethodTarget target3 = MethodTarget.of("dynamic3", pojoMethods, "Dynamic3 command", "Register Commands");
getCommandRegistry().addCommand("register dynamic1", target1);
getCommandRegistry().addCommand("register dynamic2", target2);
getCommandRegistry().addCommand("register dynamic3", target3);
return "Registered commands dynamic1, dynamic2, dynamic3";
getCommandCatalog().register(registered1, registered2, registered3);
registerFunctionCommand("register registered4");
return "Registered commands registered1, registered2, registered3, registered4";
}
@ShellMethod(key = "register remove", value = "Deregister commands", group = "Register Commands")
@ShellMethod(key = "register remove", value = "Deregister commands", group = GROUP)
public String deregister() {
getCommandRegistry().removeCommand("register dynamic1");
getCommandRegistry().removeCommand("register dynamic2");
getCommandRegistry().removeCommand("register dynamic3");
return "Deregistered commands dynamic1, dynamic2, dynamic3";
getCommandCatalog().unregister("register registered1", "register registered2", "register registered3",
"register registered4");
return "Deregistered commands registered1, registered2, registered3, registered4";
}
private void registerFunctionCommand(String command) {
Function<CommandContext, String> function = ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return String.format("hi, arg1 value is '%s'", arg1);
};
CommandRegistration registration = CommandRegistration.builder()
.command(command)
.help("registered4 command")
.group(GROUP)
.withTarget()
.function(function)
.and()
.withOption()
.longNames("arg1")
.and()
.build();
getCommandCatalog().register(registration);
}
public static class PojoMethods {
@ShellMethod
public String dynamic1() {
return "dynamic1";
public String registered1() {
return "registered1";
}
@ShellMethod
public String dynamic2(String arg1) {
return "dynamic2" + arg1;
public String registered2(String arg1) {
return "registered2" + arg1;
}
@ShellMethod
public String dynamic3(@ShellOption(defaultValue = ShellOption.NULL) String arg1) {
return "dynamic3" + arg1;
public String registered3(@ShellOption(defaultValue = ShellOption.NULL) String arg1) {
return "registered3" + arg1;
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://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.samples.standard;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandResolver;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
public class ResolvedCommands {
private static final String GROUP = "Resolve Commands";
@Configuration
public static class ResolvedCommandsConfiguration {
@Bean
Server1CommandResolver server1CommandResolver() {
return new Server1CommandResolver();
}
@Bean
Server2CommandResolver server2CommandResolver() {
return new Server2CommandResolver();
}
}
@ShellComponent
public static class ResolvedCommandsCommands {
private final Server1CommandResolver server1CommandResolver;
private final Server2CommandResolver server2CommandResolver;
ResolvedCommandsCommands(Server1CommandResolver server1CommandResolver,
Server2CommandResolver server2CommandResolver) {
this.server1CommandResolver = server1CommandResolver;
this.server2CommandResolver = server2CommandResolver;
}
@ShellMethod(key = "resolve enableserver1", group = GROUP)
public String server1Enable() {
server1CommandResolver.enabled = true;
return "Enabled server1";
}
@ShellMethod(key = "resolve disableserver1", group = GROUP)
public String server1Disable() {
server1CommandResolver.enabled = false;
return "Disabled server1";
}
@ShellMethod(key = "resolve enableserver2", group = GROUP)
public String server2Enable() {
server2CommandResolver.enabled = true;
return "Enabled server2";
}
@ShellMethod(key = "resolve disableserver2", group = GROUP)
public String server2Disable() {
server2CommandResolver.enabled = false;
return "Disabled server2";
}
}
static class Server1CommandResolver implements CommandResolver {
private final List<CommandRegistration> registrations = new ArrayList<>();
boolean enabled = false;
Server1CommandResolver() {
CommandRegistration resolved1 = CommandRegistration.builder()
.command("resolve server1 command1")
.group(GROUP)
.help("server1 command1")
.withTarget()
.function(ctx -> {
return "hi from server1 command1";
})
.and()
.build();
registrations.add(resolved1);
}
@Override
public List<CommandRegistration> resolve() {
return enabled ? registrations : Collections.emptyList();
}
}
static class Server2CommandResolver implements CommandResolver {
private final List<CommandRegistration> registrations = new ArrayList<>();
boolean enabled = false;
Server2CommandResolver() {
CommandRegistration resolved1 = CommandRegistration.builder()
.command("resolve server2 command1")
.group(GROUP)
.help("server2 command1")
.withTarget()
.function(ctx -> {
return "hi from server2 command1";
})
.and()
.build();
CommandRegistration resolved2 = CommandRegistration.builder()
.command("resolve server2 command2")
.group(GROUP)
.help("server2 command2")
.withTarget()
.function(ctx -> {
return "hi from server2 command2";
})
.and()
.build();
registrations.add(resolved1);
registrations.add(resolved2);
}
@Override
public List<CommandRegistration> resolve() {
return enabled ? registrations : Collections.emptyList();
}
}
}