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

@@ -0,0 +1,58 @@
/*
* Copyright 2021-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.boot;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.MethodTargetRegistrar;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandCatalogCustomizer;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandResolver;
@Configuration(proxyBeanMethods = false)
public class CommandCatalogAutoConfiguration {
@Bean
@ConditionalOnMissingBean(CommandCatalog.class)
public CommandCatalog commandCatalog(ObjectProvider<MethodTargetRegistrar> methodTargetRegistrars,
ObjectProvider<CommandResolver> commandResolvers,
ObjectProvider<CommandCatalogCustomizer> commandCatalogCustomizers) {
List<CommandResolver> resolvers = commandResolvers.orderedStream().collect(Collectors.toList());
CommandCatalog catalog = CommandCatalog.of(resolvers, null);
methodTargetRegistrars.orderedStream().forEach(resolver -> {
resolver.register(catalog);
});
commandCatalogCustomizers.orderedStream().forEach(customizer -> {
customizer.customize(catalog);
});
return catalog;
}
@Bean
public CommandCatalogCustomizer defaultCommandCatalogCustomizer(ObjectProvider<CommandRegistration> commandRegistrations) {
return catalog -> {
commandRegistrations.orderedStream().forEach(registration -> {
catalog.register(registration);
});
};
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2021 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.boot;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.ConfigurableCommandRegistry;
import org.springframework.shell.MethodTargetRegistrar;
import org.springframework.shell.context.ShellContext;
@Configuration(proxyBeanMethods = false)
public class CommandRegistryAutoConfiguration {
@Bean
public CommandRegistry commandRegistry(
ObjectProvider<MethodTargetRegistrar> methodTargetRegistrars,
ShellContext shellContext) {
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(shellContext);
methodTargetRegistrars.orderedStream().forEach(resolver -> {
resolver.register(registry);
});
return registry;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors.
* Copyright 2021-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.
@@ -34,7 +34,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.command.CommandCatalog;
@Configuration(proxyBeanMethods = false)
public class LineReaderAutoConfiguration {
@@ -45,7 +45,7 @@ public class LineReaderAutoConfiguration {
private Parser parser;
private CommandRegistry commandRegistry;
private CommandCatalog commandRegistry;
private org.jline.reader.History jLineHistory;
@@ -53,7 +53,7 @@ public class LineReaderAutoConfiguration {
private String historyPath;
public LineReaderAutoConfiguration(Terminal terminal, Completer completer, Parser parser,
CommandRegistry commandRegistry, org.jline.reader.History jLineHistory) {
CommandCatalog commandRegistry, org.jline.reader.History jLineHistory) {
this.terminal = terminal;
this.completer = completer;
this.parser = parser;
@@ -79,7 +79,7 @@ public class LineReaderAutoConfiguration {
public AttributedString highlight(LineReader reader, String buffer) {
int l = 0;
String best = null;
for (String command : commandRegistry.listCommands().keySet()) {
for (String command : commandRegistry.getRegistrations().keySet()) {
if (buffer.startsWith(command) && command.length() > l) {
l = command.length();
best = command;

View File

@@ -1,24 +1,35 @@
package org.springframework.shell.boot;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.shell.ParameterResolver;
import org.springframework.shell.standard.StandardParameterResolver;
import org.springframework.shell.standard.ValueProvider;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.shell.command.ArgumentHeaderMethodArgumentResolver;
import org.springframework.shell.command.CommandContextMethodArgumentResolver;
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
import org.springframework.shell.completion.CompletionResolver;
import org.springframework.shell.completion.DefaultCompletionResolver;
import org.springframework.shell.standard.ShellOptionMethodArgumentResolver;
@Configuration(proxyBeanMethods = false)
public class ParameterResolverAutoConfiguration {
@Bean
public ParameterResolver standardParameterResolver(ConversionService conversionService,
ObjectProvider<ValueProvider> valueProviders) {
Set<ValueProvider> collect = valueProviders.orderedStream().collect(Collectors.toSet());
return new StandardParameterResolver(conversionService, collect);
public CompletionResolver defaultCompletionResolver() {
return new DefaultCompletionResolver();
}
@Bean
public CommandExecutionHandlerMethodArgumentResolvers commandExecutionHandlerMethodArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
resolvers.add(new ArgumentHeaderMethodArgumentResolver(new DefaultConversionService(), null));
resolvers.add(new HeadersMethodArgumentResolver());
resolvers.add(new CommandContextMethodArgumentResolver());
resolvers.add(new ShellOptionMethodArgumentResolver(new DefaultConversionService(), null));
return new CommandExecutionHandlerMethodArgumentResolvers(resolvers);
}
}

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.
@@ -18,6 +18,8 @@ package org.springframework.shell.boot;
import java.util.Set;
import org.jline.terminal.Terminal;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.ApplicationContext;
@@ -27,10 +29,10 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.ResultHandler;
import org.springframework.shell.ResultHandlerService;
import org.springframework.shell.Shell;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.result.GenericResultHandlerService;
import org.springframework.shell.result.ResultHandlerConfig;
@@ -61,7 +63,7 @@ public class SpringShellAutoConfiguration {
}
@Bean
public Shell shell(ResultHandlerService resultHandlerService, CommandRegistry commandRegistry) {
return new Shell(resultHandlerService, commandRegistry);
public Shell shell(ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, Terminal terminal) {
return new Shell(resultHandlerService, commandRegistry, terminal);
}
}

View File

@@ -18,8 +18,8 @@ package org.springframework.shell.boot;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.MethodTargetRegistrar;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.standard.CommandValueProvider;
import org.springframework.shell.standard.EnumValueProvider;
import org.springframework.shell.standard.FileValueProvider;
@@ -35,7 +35,7 @@ import org.springframework.shell.standard.ValueProvider;
public class StandardAPIAutoConfiguration {
@Bean
public ValueProvider commandValueProvider(CommandRegistry commandRegistry) {
public ValueProvider commandValueProvider(CommandCatalog commandRegistry) {
return new CommandValueProvider(commandRegistry);
}

View File

@@ -3,7 +3,7 @@ org.springframework.shell.boot.ShellContextAutoConfiguration,\
org.springframework.shell.boot.SpringShellAutoConfiguration,\
org.springframework.shell.boot.ShellRunnerAutoConfiguration,\
org.springframework.shell.boot.ApplicationRunnerAutoConfiguration,\
org.springframework.shell.boot.CommandRegistryAutoConfiguration,\
org.springframework.shell.boot.CommandCatalogAutoConfiguration,\
org.springframework.shell.boot.LineReaderAutoConfiguration,\
org.springframework.shell.boot.CompleterAutoConfiguration,\
org.springframework.shell.boot.JLineAutoConfiguration,\

View File

@@ -0,0 +1,106 @@
/*
* 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.boot;
import java.util.Collections;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandResolver;
import static org.assertj.core.api.Assertions.assertThat;
public class CommandCatalogAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CommandCatalogAutoConfiguration.class));
@Test
void defaultCommandCatalog() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(CommandCatalog.class));
}
@Test
void testCommandResolvers() {
this.contextRunner.withUserConfiguration(CustomCommandResolverConfiguration.class)
.run((context) -> {
CommandCatalog commandCatalog = context.getBean(CommandCatalog.class);
assertThat(commandCatalog).extracting("resolvers").asInstanceOf(InstanceOfAssertFactories.LIST)
.hasSize(1);
});
}
@Test
void customCommandCatalog() {
this.contextRunner.withUserConfiguration(CustomCommandCatalogConfiguration.class)
.run((context) -> {
CommandCatalog commandCatalog = context.getBean(CommandCatalog.class);
assertThat(commandCatalog).isSameAs(CustomCommandCatalogConfiguration.testCommandCatalog);
});
}
@Test
void registerCommandRegistration() {
this.contextRunner.withUserConfiguration(CustomCommandRegistrationConfiguration.class)
.run((context) -> {
CommandCatalog commandCatalog = context.getBean(CommandCatalog.class);
assertThat(commandCatalog.getRegistrations().get("customcommand")).isNotNull();
});
}
@Configuration
static class CustomCommandResolverConfiguration {
@Bean
CommandResolver customCommandResolver() {
return () -> Collections.emptyList();
}
}
@Configuration
static class CustomCommandCatalogConfiguration {
static final CommandCatalog testCommandCatalog = CommandCatalog.of();
@Bean
CommandCatalog customCommandCatalog() {
return testCommandCatalog;
}
}
@Configuration
static class CustomCommandRegistrationConfiguration {
@Bean
CommandRegistration commandRegistration() {
return CommandRegistration.builder()
.command("customcommand")
.withTarget()
.function(ctx -> {
return null;
})
.and()
.build();
}
}
}

View File

@@ -22,8 +22,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.shell.ParameterResolver;
import org.springframework.shell.Shell;
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
import org.springframework.shell.completion.CompletionResolver;
import org.springframework.shell.context.ShellContext;
import org.springframework.shell.jline.InteractiveShellRunner;
import org.springframework.shell.jline.NonInteractiveShellRunner;
@@ -46,7 +47,8 @@ class ShellRunnerAutoConfigurationTests {
.withBean(LineReader.class, () -> mock(LineReader.class))
.withBean(Parser.class, () -> mock(Parser.class))
.withBean(ShellContext.class, () -> mock(ShellContext.class))
.withBean(ParameterResolver.class, () -> mock(ParameterResolver.class));
.withBean(CompletionResolver.class, () -> mock(CompletionResolver.class))
.withBean(CommandExecutionHandlerMethodArgumentResolvers.class, () -> mock(CommandExecutionHandlerMethodArgumentResolvers.class));
@Nested
class Interactive {