Skip to content

Commit 8a23518

Browse files
committed
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
1 parent 81e5bf8 commit 8a23518

File tree

91 files changed

+7029
-2294
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+7029
-2294
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ target/
1010
*.iws
1111
.DS_Store
1212
spring-shell.log
13+
shell.log
14+
shell.log.*.gz
1315

1416
# Visual Studio Code
1517
.vscode/*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2021-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.boot;
17+
18+
import java.util.List;
19+
import java.util.stream.Collectors;
20+
21+
import org.springframework.beans.factory.ObjectProvider;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.shell.MethodTargetRegistrar;
26+
import org.springframework.shell.command.CommandCatalog;
27+
import org.springframework.shell.command.CommandCatalogCustomizer;
28+
import org.springframework.shell.command.CommandRegistration;
29+
import org.springframework.shell.command.CommandResolver;
30+
31+
@Configuration(proxyBeanMethods = false)
32+
public class CommandCatalogAutoConfiguration {
33+
34+
@Bean
35+
@ConditionalOnMissingBean(CommandCatalog.class)
36+
public CommandCatalog commandCatalog(ObjectProvider<MethodTargetRegistrar> methodTargetRegistrars,
37+
ObjectProvider<CommandResolver> commandResolvers,
38+
ObjectProvider<CommandCatalogCustomizer> commandCatalogCustomizers) {
39+
List<CommandResolver> resolvers = commandResolvers.orderedStream().collect(Collectors.toList());
40+
CommandCatalog catalog = CommandCatalog.of(resolvers, null);
41+
methodTargetRegistrars.orderedStream().forEach(resolver -> {
42+
resolver.register(catalog);
43+
});
44+
commandCatalogCustomizers.orderedStream().forEach(customizer -> {
45+
customizer.customize(catalog);
46+
});
47+
return catalog;
48+
}
49+
50+
@Bean
51+
public CommandCatalogCustomizer defaultCommandCatalogCustomizer(ObjectProvider<CommandRegistration> commandRegistrations) {
52+
return catalog -> {
53+
commandRegistrations.orderedStream().forEach(registration -> {
54+
catalog.register(registration);
55+
});
56+
};
57+
}
58+
}

spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandRegistryAutoConfiguration.java

Lines changed: 0 additions & 39 deletions
This file was deleted.

spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/LineReaderAutoConfiguration.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 the original author or authors.
2+
* Copyright 2021-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,7 +34,7 @@
3434
import org.springframework.context.annotation.Configuration;
3535
import org.springframework.context.event.ContextClosedEvent;
3636
import org.springframework.context.event.EventListener;
37-
import org.springframework.shell.CommandRegistry;
37+
import org.springframework.shell.command.CommandCatalog;
3838

3939
@Configuration(proxyBeanMethods = false)
4040
public class LineReaderAutoConfiguration {
@@ -45,15 +45,15 @@ public class LineReaderAutoConfiguration {
4545

4646
private Parser parser;
4747

48-
private CommandRegistry commandRegistry;
48+
private CommandCatalog commandRegistry;
4949

5050
private org.jline.reader.History jLineHistory;
5151

5252
@Value("${spring.application.name:spring-shell}.log")
5353
private String historyPath;
5454

5555
public LineReaderAutoConfiguration(Terminal terminal, Completer completer, Parser parser,
56-
CommandRegistry commandRegistry, org.jline.reader.History jLineHistory) {
56+
CommandCatalog commandRegistry, org.jline.reader.History jLineHistory) {
5757
this.terminal = terminal;
5858
this.completer = completer;
5959
this.parser = parser;
@@ -79,7 +79,7 @@ public LineReader lineReader() {
7979
public AttributedString highlight(LineReader reader, String buffer) {
8080
int l = 0;
8181
String best = null;
82-
for (String command : commandRegistry.listCommands().keySet()) {
82+
for (String command : commandRegistry.getRegistrations().keySet()) {
8383
if (buffer.startsWith(command) && command.length() > l) {
8484
l = command.length();
8585
best = command;
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,35 @@
11
package org.springframework.shell.boot;
22

3-
import java.util.Set;
4-
import java.util.stream.Collectors;
3+
import java.util.ArrayList;
4+
import java.util.List;
55

6-
import org.springframework.beans.factory.ObjectProvider;
76
import org.springframework.context.annotation.Bean;
87
import org.springframework.context.annotation.Configuration;
9-
import org.springframework.core.convert.ConversionService;
10-
import org.springframework.shell.ParameterResolver;
11-
import org.springframework.shell.standard.StandardParameterResolver;
12-
import org.springframework.shell.standard.ValueProvider;
8+
import org.springframework.core.convert.support.DefaultConversionService;
9+
import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver;
10+
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
11+
import org.springframework.shell.command.ArgumentHeaderMethodArgumentResolver;
12+
import org.springframework.shell.command.CommandContextMethodArgumentResolver;
13+
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
14+
import org.springframework.shell.completion.CompletionResolver;
15+
import org.springframework.shell.completion.DefaultCompletionResolver;
16+
import org.springframework.shell.standard.ShellOptionMethodArgumentResolver;
1317

1418
@Configuration(proxyBeanMethods = false)
1519
public class ParameterResolverAutoConfiguration {
1620

1721
@Bean
18-
public ParameterResolver standardParameterResolver(ConversionService conversionService,
19-
ObjectProvider<ValueProvider> valueProviders) {
20-
Set<ValueProvider> collect = valueProviders.orderedStream().collect(Collectors.toSet());
21-
return new StandardParameterResolver(conversionService, collect);
22+
public CompletionResolver defaultCompletionResolver() {
23+
return new DefaultCompletionResolver();
2224
}
2325

26+
@Bean
27+
public CommandExecutionHandlerMethodArgumentResolvers commandExecutionHandlerMethodArgumentResolvers() {
28+
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
29+
resolvers.add(new ArgumentHeaderMethodArgumentResolver(new DefaultConversionService(), null));
30+
resolvers.add(new HeadersMethodArgumentResolver());
31+
resolvers.add(new CommandContextMethodArgumentResolver());
32+
resolvers.add(new ShellOptionMethodArgumentResolver(new DefaultConversionService(), null));
33+
return new CommandExecutionHandlerMethodArgumentResolvers(resolvers);
34+
}
2435
}

spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellAutoConfiguration.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2021 the original author or authors.
2+
* Copyright 2017-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,8 @@
1818

1919
import java.util.Set;
2020

21+
import org.jline.terminal.Terminal;
22+
2123
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2224
import org.springframework.boot.convert.ApplicationConversionService;
2325
import org.springframework.context.ApplicationContext;
@@ -27,10 +29,10 @@
2729
import org.springframework.core.convert.ConversionService;
2830
import org.springframework.core.convert.support.DefaultConversionService;
2931
import org.springframework.format.support.FormattingConversionService;
30-
import org.springframework.shell.CommandRegistry;
3132
import org.springframework.shell.ResultHandler;
3233
import org.springframework.shell.ResultHandlerService;
3334
import org.springframework.shell.Shell;
35+
import org.springframework.shell.command.CommandCatalog;
3436
import org.springframework.shell.result.GenericResultHandlerService;
3537
import org.springframework.shell.result.ResultHandlerConfig;
3638

@@ -61,7 +63,7 @@ public ResultHandlerService resultHandlerService(Set<ResultHandler<?>> resultHan
6163
}
6264

6365
@Bean
64-
public Shell shell(ResultHandlerService resultHandlerService, CommandRegistry commandRegistry) {
65-
return new Shell(resultHandlerService, commandRegistry);
66+
public Shell shell(ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, Terminal terminal) {
67+
return new Shell(resultHandlerService, commandRegistry, terminal);
6668
}
6769
}

spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardAPIAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
import org.springframework.context.annotation.Bean;
2020
import org.springframework.context.annotation.Configuration;
21-
import org.springframework.shell.CommandRegistry;
2221
import org.springframework.shell.MethodTargetRegistrar;
22+
import org.springframework.shell.command.CommandCatalog;
2323
import org.springframework.shell.standard.CommandValueProvider;
2424
import org.springframework.shell.standard.EnumValueProvider;
2525
import org.springframework.shell.standard.FileValueProvider;
@@ -35,7 +35,7 @@
3535
public class StandardAPIAutoConfiguration {
3636

3737
@Bean
38-
public ValueProvider commandValueProvider(CommandRegistry commandRegistry) {
38+
public ValueProvider commandValueProvider(CommandCatalog commandRegistry) {
3939
return new CommandValueProvider(commandRegistry);
4040
}
4141

spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ org.springframework.shell.boot.ShellContextAutoConfiguration,\
33
org.springframework.shell.boot.SpringShellAutoConfiguration,\
44
org.springframework.shell.boot.ShellRunnerAutoConfiguration,\
55
org.springframework.shell.boot.ApplicationRunnerAutoConfiguration,\
6-
org.springframework.shell.boot.CommandRegistryAutoConfiguration,\
6+
org.springframework.shell.boot.CommandCatalogAutoConfiguration,\
77
org.springframework.shell.boot.LineReaderAutoConfiguration,\
88
org.springframework.shell.boot.CompleterAutoConfiguration,\
99
org.springframework.shell.boot.JLineAutoConfiguration,\
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.boot;
17+
18+
import java.util.Collections;
19+
20+
import org.assertj.core.api.InstanceOfAssertFactories;
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.boot.autoconfigure.AutoConfigurations;
24+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.shell.command.CommandCatalog;
28+
import org.springframework.shell.command.CommandRegistration;
29+
import org.springframework.shell.command.CommandResolver;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
33+
public class CommandCatalogAutoConfigurationTests {
34+
35+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
36+
.withConfiguration(AutoConfigurations.of(CommandCatalogAutoConfiguration.class));
37+
38+
@Test
39+
void defaultCommandCatalog() {
40+
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(CommandCatalog.class));
41+
}
42+
43+
@Test
44+
void testCommandResolvers() {
45+
this.contextRunner.withUserConfiguration(CustomCommandResolverConfiguration.class)
46+
.run((context) -> {
47+
CommandCatalog commandCatalog = context.getBean(CommandCatalog.class);
48+
assertThat(commandCatalog).extracting("resolvers").asInstanceOf(InstanceOfAssertFactories.LIST)
49+
.hasSize(1);
50+
});
51+
}
52+
53+
@Test
54+
void customCommandCatalog() {
55+
this.contextRunner.withUserConfiguration(CustomCommandCatalogConfiguration.class)
56+
.run((context) -> {
57+
CommandCatalog commandCatalog = context.getBean(CommandCatalog.class);
58+
assertThat(commandCatalog).isSameAs(CustomCommandCatalogConfiguration.testCommandCatalog);
59+
});
60+
}
61+
62+
@Test
63+
void registerCommandRegistration() {
64+
this.contextRunner.withUserConfiguration(CustomCommandRegistrationConfiguration.class)
65+
.run((context) -> {
66+
CommandCatalog commandCatalog = context.getBean(CommandCatalog.class);
67+
assertThat(commandCatalog.getRegistrations().get("customcommand")).isNotNull();
68+
});
69+
}
70+
71+
@Configuration
72+
static class CustomCommandResolverConfiguration {
73+
74+
@Bean
75+
CommandResolver customCommandResolver() {
76+
return () -> Collections.emptyList();
77+
}
78+
}
79+
80+
@Configuration
81+
static class CustomCommandCatalogConfiguration {
82+
83+
static final CommandCatalog testCommandCatalog = CommandCatalog.of();
84+
85+
@Bean
86+
CommandCatalog customCommandCatalog() {
87+
return testCommandCatalog;
88+
}
89+
}
90+
91+
@Configuration
92+
static class CustomCommandRegistrationConfiguration {
93+
94+
@Bean
95+
CommandRegistration commandRegistration() {
96+
return CommandRegistration.builder()
97+
.command("customcommand")
98+
.withTarget()
99+
.function(ctx -> {
100+
return null;
101+
})
102+
.and()
103+
.build();
104+
}
105+
}
106+
}

spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/ShellRunnerAutoConfigurationTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222

2323
import org.springframework.boot.autoconfigure.AutoConfigurations;
2424
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
25-
import org.springframework.shell.ParameterResolver;
2625
import org.springframework.shell.Shell;
26+
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
27+
import org.springframework.shell.completion.CompletionResolver;
2728
import org.springframework.shell.context.ShellContext;
2829
import org.springframework.shell.jline.InteractiveShellRunner;
2930
import org.springframework.shell.jline.NonInteractiveShellRunner;
@@ -46,7 +47,8 @@ class ShellRunnerAutoConfigurationTests {
4647
.withBean(LineReader.class, () -> mock(LineReader.class))
4748
.withBean(Parser.class, () -> mock(Parser.class))
4849
.withBean(ShellContext.class, () -> mock(ShellContext.class))
49-
.withBean(ParameterResolver.class, () -> mock(ParameterResolver.class));
50+
.withBean(CompletionResolver.class, () -> mock(CompletionResolver.class))
51+
.withBean(CommandExecutionHandlerMethodArgumentResolvers.class, () -> mock(CommandExecutionHandlerMethodArgumentResolvers.class));
5052

5153
@Nested
5254
class Interactive {

0 commit comments

Comments
 (0)