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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,6 +10,8 @@ target/
|
||||
*.iws
|
||||
.DS_Store
|
||||
spring-shell.log
|
||||
shell.log
|
||||
shell.log.*.gz
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,\
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-messaging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jline</groupId>
|
||||
<artifactId>jline</artifactId>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementing this interface allows sub-systems (such as the {@literal help} command) to
|
||||
* discover available commands.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandRegistry {
|
||||
|
||||
/**
|
||||
* Return the mapping from command trigger keywords to implementation.
|
||||
*/
|
||||
Map<String, MethodTarget> listCommands();
|
||||
|
||||
/**
|
||||
* Register a new command.
|
||||
*
|
||||
* @param name the command name
|
||||
* @param target the method target
|
||||
*/
|
||||
void addCommand(String name, MethodTarget target);
|
||||
|
||||
/**
|
||||
* Deregister a command.
|
||||
*
|
||||
* @param name the command name
|
||||
*/
|
||||
void removeCommand(String name);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.context.ShellContext;
|
||||
|
||||
/**
|
||||
* A {@link CommandRegistry} that supports registration of new commands.
|
||||
*
|
||||
* <p>Makes sure that no two commands are registered with the same name.</p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ConfigurableCommandRegistry implements CommandRegistry {
|
||||
|
||||
private final ShellContext shellContext;
|
||||
private Map<String, MethodTarget> commands = new HashMap<>();
|
||||
|
||||
public ConfigurableCommandRegistry(ShellContext shellContext) {
|
||||
this.shellContext = shellContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> listCommands() {
|
||||
return commands.entrySet().stream()
|
||||
.filter(e -> {
|
||||
InteractionMode mim = e.getValue().getInteractionMode();
|
||||
InteractionMode cim = shellContext.getInteractionMode();
|
||||
if (mim == null || cim == null || mim == InteractionMode.ALL) {
|
||||
return true;
|
||||
}
|
||||
else if (mim == InteractionMode.INTERACTIVE) {
|
||||
return cim == InteractionMode.INTERACTIVE || cim == InteractionMode.ALL;
|
||||
}
|
||||
else if (mim == InteractionMode.NONINTERACTIVE) {
|
||||
return cim == InteractionMode.NONINTERACTIVE || cim == InteractionMode.ALL;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCommand(String name, MethodTarget target) {
|
||||
commands.put(name, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCommand(String name) {
|
||||
commands.remove(name);
|
||||
}
|
||||
|
||||
public void register(String name, MethodTarget target) {
|
||||
MethodTarget previous = commands.get(name);
|
||||
if (previous != null) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Illegal registration for command '%s': Attempt to register both '%s' and '%s'", name, target, previous));
|
||||
}
|
||||
commands.put(name, target);
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-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;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Represents a shell command behavior, <em>i.e.</em> code to be executed when a command is requested.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class MethodTarget implements Command {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Object bean;
|
||||
|
||||
private final Help help;
|
||||
|
||||
private final InteractionMode interactionMode;
|
||||
|
||||
/**
|
||||
* If not null, returns whether or not the command is currently available. Implementations must be idempotent.
|
||||
*/
|
||||
private final Supplier<Availability> availabilityIndicator;
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help) {
|
||||
this(method, bean, new Help(help, null), null);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, String help, Supplier<Availability> availabilityIndicator) {
|
||||
this(method, bean, new Help(help, null), availabilityIndicator);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, Help help, Supplier<Availability> availabilityIndicator) {
|
||||
this(method, bean, help, availabilityIndicator, null);
|
||||
}
|
||||
|
||||
public MethodTarget(Method method, Object bean, Help help, Supplier<Availability> availabilityIndicator, InteractionMode interactionMode) {
|
||||
Assert.notNull(method, "Method cannot be null");
|
||||
Assert.notNull(bean, "Bean cannot be null");
|
||||
Assert.hasText(help.getDescription(), String.format("Help cannot be blank when trying to define command based on '%s'", method));
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
this.method = method;
|
||||
this.bean = bean;
|
||||
this.help = help;
|
||||
this.availabilityIndicator = availabilityIndicator != null ? availabilityIndicator : () -> Availability.available();
|
||||
this.interactionMode = interactionMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, String description, String group) {
|
||||
return of(name, bean, new Help(description, group));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, Help help) {
|
||||
return of(name, bean, help, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a MethodTarget for the unique method named {@literal name} on the given object. Fails with an exception
|
||||
* in case of overloaded method.
|
||||
*/
|
||||
public static MethodTarget of(String name, Object bean, Help help, Supplier<Availability> availabilityIndicator) {
|
||||
Set<Method> found = new HashSet<>();
|
||||
ReflectionUtils.doWithMethods(bean.getClass(), found::add, m -> m.getName().equals(name));
|
||||
if (found.size() != 1) {
|
||||
throw new IllegalArgumentException(String.format("Could not find unique method named '%s' on object of class %s. Found %s",
|
||||
name, bean.getClass(), found));
|
||||
}
|
||||
return new MethodTarget(found.iterator().next(), bean, help, availabilityIndicator);
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public Object getBean() {
|
||||
return bean;
|
||||
}
|
||||
|
||||
public String getHelp() {
|
||||
return help.getDescription();
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return help.getGroup();
|
||||
}
|
||||
|
||||
public Availability getAvailability() {
|
||||
return availabilityIndicator.get();
|
||||
}
|
||||
|
||||
public InteractionMode getInteractionMode() {
|
||||
return interactionMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
MethodTarget that = (MethodTarget) o;
|
||||
|
||||
if (!method.equals(that.method)) return false;
|
||||
if (!bean.equals(that.bean)) return false;
|
||||
if (!help.equals(that.help)) return false;
|
||||
return help.equals(that.help);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = method.hashCode();
|
||||
result = 31 * result + bean.hashCode();
|
||||
result = 31 * result + help.hashCode();
|
||||
result = 31 * result + help.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return method.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2017 the original author or authors.
|
||||
* Copyright 2015-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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
|
||||
/**
|
||||
* Strategy interface for registering commands.
|
||||
*
|
||||
@@ -27,6 +29,6 @@ public interface MethodTargetRegistrar {
|
||||
/**
|
||||
* Register mappings from {@literal <command keyword(s)>} to actual behavior.
|
||||
*/
|
||||
void register(ConfigurableCommandRegistry registry);
|
||||
void register(CommandCatalog registry);
|
||||
|
||||
}
|
||||
|
||||
@@ -34,15 +34,10 @@ import javax.validation.metadata.ElementDescriptor;
|
||||
*/
|
||||
public class ParameterDescription {
|
||||
|
||||
/**
|
||||
* The original method parameter this is describing.
|
||||
*/
|
||||
private final MethodParameter parameter;
|
||||
|
||||
/**
|
||||
* A string representation of the type of the parameter.
|
||||
*/
|
||||
private final String type;
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* A string representation of the parameter, as it should appear in a parameter list.
|
||||
@@ -85,15 +80,8 @@ public class ParameterDescription {
|
||||
*/
|
||||
private ElementDescriptor elementDescriptor;
|
||||
|
||||
public ParameterDescription(MethodParameter parameter, String type) {
|
||||
this.parameter = parameter;
|
||||
public void type(String type) {
|
||||
this.type = type;
|
||||
this.formal = type;
|
||||
}
|
||||
|
||||
public static ParameterDescription outOf(MethodParameter parameter) {
|
||||
Class<?> type = parameter.getParameterType();
|
||||
return new ParameterDescription(parameter, Utils.unCamelify(type.getSimpleName()));
|
||||
}
|
||||
|
||||
public ParameterDescription help(String help) {
|
||||
@@ -171,17 +159,13 @@ public class ParameterDescription {
|
||||
return String.format("%s %s", keys.isEmpty() ? "" : keys().iterator().next(), formal());
|
||||
}
|
||||
|
||||
public MethodParameter parameter() {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ParameterDescription that = (ParameterDescription) o;
|
||||
return mandatoryKey == that.mandatoryKey &&
|
||||
Objects.equals(parameter, that.parameter) &&
|
||||
// Objects.equals(parameter, that.parameter) &&
|
||||
Objects.equals(type, that.type) &&
|
||||
Objects.equals(formal, that.formal) &&
|
||||
Objects.equals(defaultValue, that.defaultValue) &&
|
||||
@@ -192,6 +176,6 @@ public class ParameterDescription {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(parameter, type, formal, defaultValue, defaultValueWhenFlag, keys, mandatoryKey, help);
|
||||
return Objects.hash(type, formal, defaultValue, defaultValueWhenFlag, keys, mandatoryKey, help);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2017 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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible, once the command has been identified, of transforming the textual
|
||||
* input to an actual parameter object.
|
||||
*
|
||||
* <p>
|
||||
* An order can also be specified in case more than one {@link ParameterResolver} supports a {@link MethodParameter}.
|
||||
* See {@link AnnotationAwareOrderComparator} for details..
|
||||
* </p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Camilo Gonzalez
|
||||
*/
|
||||
public interface ParameterResolver {
|
||||
|
||||
/**
|
||||
* Should return true if this resolver recognizes the given method parameter (<em>e.g.</em> it
|
||||
* has the correct annotation or the correct type).
|
||||
*/
|
||||
boolean supports(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Turn the given textual input into an actual object, maybe using some conversion or lookup mechanism.
|
||||
*/
|
||||
ValueResult resolve(MethodParameter methodParameter, List<String> words);
|
||||
|
||||
/**
|
||||
* Describe a supported parameter, so that integrated help can be generated.
|
||||
* <p>Typical implementations will return a one element stream result, but some may return several (for
|
||||
* example if binding several words to a POJO).</p>
|
||||
*/
|
||||
Stream<ParameterDescription> describe(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Invoked during TAB completion. If the {@link CompletionContext} can be interpreted as the start
|
||||
* of a supported {@link MethodParameter} value, one or several proposals should be returned.
|
||||
*/
|
||||
List<CompletionProposal> complete(MethodParameter parameter, CompletionContext context);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 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;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
@@ -23,21 +22,17 @@ import java.util.Set;
|
||||
* Thrown when one or more parameters fail bean validation constraints.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ParameterValidationException extends RuntimeException {
|
||||
private final Set<ConstraintViolation<Object>> constraintViolations;
|
||||
private final MethodTarget methodTarget;
|
||||
|
||||
public ParameterValidationException(Set<ConstraintViolation<Object>> constraintViolations, MethodTarget methodTarget) {
|
||||
private final Set<ConstraintViolation<Object>> constraintViolations;
|
||||
|
||||
public ParameterValidationException(Set<ConstraintViolation<Object>> constraintViolations) {
|
||||
this.constraintViolations = constraintViolations;
|
||||
this.methodTarget = methodTarget;
|
||||
}
|
||||
|
||||
public Set<ConstraintViolation<Object>> getConstraintViolations() {
|
||||
return constraintViolations;
|
||||
}
|
||||
|
||||
public MethodTarget getMethodTarget() {
|
||||
return methodTarget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 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,45 +13,36 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.ValidatorFactory;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.Signals;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandExecution;
|
||||
import org.springframework.shell.command.CommandExecution.CommandExecutionException;
|
||||
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
|
||||
/**
|
||||
* Main class implementing a shell loop.
|
||||
*
|
||||
* <p>
|
||||
* Given some textual input, locate the {@link MethodTarget} to invoke and
|
||||
* {@link ResultHandler#handleResult(Object) handle} the result.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Also provides hooks for code completion
|
||||
* </p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@@ -66,11 +57,10 @@ public class Shell {
|
||||
*/
|
||||
public static final Object NO_INPUT = new Object();
|
||||
|
||||
private final CommandRegistry commandRegistry;
|
||||
|
||||
private Validator validator = Utils.defaultValidator();
|
||||
|
||||
protected List<ParameterResolver> parameterResolvers;
|
||||
private final Terminal terminal;
|
||||
private final CommandCatalog commandRegistry;
|
||||
protected List<CompletionResolver> completionResolvers = new ArrayList<>();
|
||||
private CommandExecutionHandlerMethodArgumentResolvers argumentResolvers;
|
||||
|
||||
/**
|
||||
* Marker object to distinguish unresolved arguments from {@code null}, which is a valid
|
||||
@@ -78,9 +68,23 @@ public class Shell {
|
||||
*/
|
||||
protected static final Object UNRESOLVED = new Object();
|
||||
|
||||
public Shell(ResultHandlerService resultHandlerService, CommandRegistry commandRegistry) {
|
||||
private Validator validator = Utils.defaultValidator();
|
||||
|
||||
public Shell(ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, Terminal terminal) {
|
||||
this.resultHandlerService = resultHandlerService;
|
||||
this.commandRegistry = commandRegistry;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setCompletionResolvers(List<CompletionResolver> resolvers) {
|
||||
this.completionResolvers = new ArrayList<>(resolvers);
|
||||
AnnotationAwareOrderComparator.sort(completionResolvers);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setArgumentResolvers(CommandExecutionHandlerMethodArgumentResolvers argumentResolvers) {
|
||||
this.argumentResolvers = argumentResolvers;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
@@ -88,12 +92,6 @@ public class Shell {
|
||||
this.validator = validatorFactory.getValidator();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setParameterResolvers(List<ParameterResolver> resolvers) {
|
||||
this.parameterResolvers = new ArrayList<>(resolvers);
|
||||
AnnotationAwareOrderComparator.sort(parameterResolvers);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main program loop: acquire input, try to match it to a command and evaluate. Repeat
|
||||
* until a {@link ResultHandler} causes the process to exit or there is no input.
|
||||
@@ -145,21 +143,24 @@ public class Shell {
|
||||
String command = findLongestCommand(line);
|
||||
|
||||
List<String> words = input.words();
|
||||
log.debug("Evaluate input with line=[{}], command=[{}]", line, command);
|
||||
if (command != null) {
|
||||
Map<String, MethodTarget> methodTargets = commandRegistry.listCommands();
|
||||
MethodTarget methodTarget = methodTargets.get(command);
|
||||
Availability availability = methodTarget.getAvailability();
|
||||
if (availability.isAvailable()) {
|
||||
|
||||
Optional<CommandRegistration> commandRegistration = commandRegistry.getRegistrations().values().stream()
|
||||
.filter(r -> {
|
||||
return r.getCommand().equals(command);
|
||||
})
|
||||
.findFirst();
|
||||
|
||||
if (commandRegistration.isPresent()) {
|
||||
List<String> wordsForArgs = wordsForArguments(command, words);
|
||||
Method method = methodTarget.getMethod();
|
||||
|
||||
Thread commandThread = Thread.currentThread();
|
||||
Object sh = Signals.register("INT", () -> commandThread.interrupt());
|
||||
try {
|
||||
Object[] args = resolveArgs(method, wordsForArgs);
|
||||
validateArgs(args, methodTarget);
|
||||
|
||||
return ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
|
||||
CommandExecution execution = CommandExecution
|
||||
.of(argumentResolvers != null ? argumentResolvers.getResolvers() : null, validator, terminal);
|
||||
return execution.evaluate(commandRegistration.get(), wordsForArgs.toArray(new String[0]));
|
||||
}
|
||||
catch (UndeclaredThrowableException e) {
|
||||
if (e.getCause() instanceof InterruptedException || e.getCause() instanceof ClosedByInterruptException) {
|
||||
@@ -167,6 +168,9 @@ public class Shell {
|
||||
}
|
||||
return e.getCause();
|
||||
}
|
||||
catch (CommandExecutionException e) {
|
||||
return e.getCause();
|
||||
}
|
||||
catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
@@ -175,7 +179,7 @@ public class Shell {
|
||||
}
|
||||
}
|
||||
else {
|
||||
return new CommandNotCurrentlyAvailable(command, availability);
|
||||
return new CommandNotFound(words);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -183,6 +187,7 @@ public class Shell {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return true if the parsed input ends up being empty (<em>e.g.</em> hitting ENTER on an
|
||||
* empty line or blank space).
|
||||
@@ -229,18 +234,11 @@ public class Shell {
|
||||
if (best != null) {
|
||||
CompletionContext argsContext = context.drop(best.split(" ").length);
|
||||
// Try to complete arguments
|
||||
Map<String, MethodTarget> methodTargets = commandRegistry.listCommands();
|
||||
MethodTarget methodTarget = methodTargets.get(best);
|
||||
Method method = methodTarget.getMethod();
|
||||
CommandRegistration registration = commandRegistry.getRegistrations().get(best);
|
||||
|
||||
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
|
||||
for (ParameterResolver resolver : parameterResolvers) {
|
||||
for (int index = 0; index < parameters.size(); index++) {
|
||||
MethodParameter parameter = parameters.get(index);
|
||||
if (resolver.supports(parameter)) {
|
||||
resolver.complete(parameter, argsContext).stream().forEach(candidates::add);
|
||||
}
|
||||
}
|
||||
for (CompletionResolver resolver : completionResolvers) {
|
||||
List<CompletionProposal> resolved = resolver.resolve(registration, argsContext);
|
||||
candidates.addAll(resolved);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
@@ -250,61 +248,23 @@ public class Shell {
|
||||
// Workaround for https://github.com/spring-projects/spring-shell/issues/150
|
||||
// (sadly, this ties this class to JLine somehow)
|
||||
int lastWordStart = prefix.lastIndexOf(' ') + 1;
|
||||
Map<String, MethodTarget> methodTargets = commandRegistry.listCommands();
|
||||
return methodTargets.entrySet().stream()
|
||||
.filter(e -> e.getKey().startsWith(prefix))
|
||||
.map(e -> toCommandProposal(e.getKey().substring(lastWordStart), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
return commandRegistry.getRegistrations().values().stream()
|
||||
.filter(r -> {
|
||||
return r.getCommand().startsWith(prefix);
|
||||
})
|
||||
.map(r -> {
|
||||
String c = r.getCommand();
|
||||
c = c.substring(lastWordStart);
|
||||
return toCommandProposal(c, r);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CompletionProposal toCommandProposal(String command, MethodTarget methodTarget) {
|
||||
private CompletionProposal toCommandProposal(String command, CommandRegistration registration) {
|
||||
return new CompletionProposal(command)
|
||||
.dontQuote(true)
|
||||
.category("Available commands")
|
||||
.description(methodTarget.getHelp());
|
||||
}
|
||||
|
||||
private void validateArgs(Object[] args, MethodTarget methodTarget) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] == UNRESOLVED) {
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(methodTarget.getMethod(), i);
|
||||
throw new IllegalStateException("Could not resolve " + methodParameter);
|
||||
}
|
||||
}
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables().validateParameters(
|
||||
methodTarget.getBean(),
|
||||
methodTarget.getMethod(),
|
||||
args);
|
||||
if (constraintViolations.size() > 0) {
|
||||
throw new ParameterValidationException(constraintViolations, methodTarget);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use all known {@link ParameterResolver}s to try to compute a value for each parameter
|
||||
* of the method to invoke.
|
||||
* @param method the method for which parameters should be computed
|
||||
* @param wordsForArgs the list of 'words' that should be converted to parameter values.
|
||||
* May include markers for passing parameters 'by name'
|
||||
* @return an array containing resolved parameter values, or {@link #UNRESOLVED} for
|
||||
* parameters that could not be resolved
|
||||
*/
|
||||
private Object[] resolveArgs(Method method, List<String> wordsForArgs) {
|
||||
log.debug("Resolving args {} {}", method, wordsForArgs);
|
||||
List<MethodParameter> parameters = Utils.createMethodParameters(method).collect(Collectors.toList());
|
||||
Object[] args = new Object[parameters.size()];
|
||||
Arrays.fill(args, UNRESOLVED);
|
||||
for (ParameterResolver resolver : parameterResolvers) {
|
||||
log.debug("Resolving args with {}", resolver);
|
||||
for (int argIndex = 0; argIndex < args.length; argIndex++) {
|
||||
MethodParameter parameter = parameters.get(argIndex);
|
||||
if (args[argIndex] == UNRESOLVED && resolver.supports(parameter)) {
|
||||
args[argIndex] = resolver.resolve(parameter, wordsForArgs).resolvedValue();
|
||||
log.debug("Resolved {} {} {} {}", method, args[argIndex], resolver, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
return args;
|
||||
.description(registration.getHelp());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,8 +273,7 @@ public class Shell {
|
||||
* @return a valid command name, or {@literal null} if none matched
|
||||
*/
|
||||
private String findLongestCommand(String prefix) {
|
||||
Map<String, MethodTarget> methodTargets = commandRegistry.listCommands();
|
||||
String result = methodTargets.keySet().stream()
|
||||
String result = commandRegistry.getRegistrations().keySet().stream()
|
||||
.filter(command -> prefix.equals(command) || prefix.startsWith(command + " "))
|
||||
.reduce("", (c1, c2) -> c1.length() > c2.length() ? c1 : c2);
|
||||
return "".equals(result) ? null : result;
|
||||
|
||||
@@ -20,7 +20,10 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
@@ -130,4 +133,37 @@ public class Utils {
|
||||
public static Validator defaultValidator() {
|
||||
return DEFAULT_VALIDATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split array into list of lists by predicate
|
||||
*
|
||||
* @param array the array
|
||||
* @param predicate the predicate
|
||||
* @return the list of lists
|
||||
*/
|
||||
public static <T> List<List<T>> split(T[] array, Predicate<T> predicate) {
|
||||
List<T> list = Arrays.asList(array);
|
||||
boolean[] boundaries = new boolean[array.length];
|
||||
List<List<T>> split = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
boundaries[i] = predicate.test(array[i]);
|
||||
}
|
||||
|
||||
int tail = 0;
|
||||
for (int i = 0; i < boundaries.length; i++) {
|
||||
if (boundaries[i]) {
|
||||
if (tail < i) {
|
||||
split.add(list.subList(tail, i));
|
||||
}
|
||||
tail = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (tail < array.length) {
|
||||
split.add(list.subList(tail, array.length));
|
||||
}
|
||||
|
||||
return split;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandlingException;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.shell.support.AbstractArgumentMethodArgumentResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Resolver for {@link Header @Header} arguments.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ArgumentHeaderMethodArgumentResolver extends AbstractArgumentMethodArgumentResolver {
|
||||
|
||||
public ArgumentHeaderMethodArgumentResolver(ConversionService conversionService,
|
||||
@Nullable ConfigurableBeanFactory beanFactory) {
|
||||
super(conversionService, beanFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(Header.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
|
||||
Header annot = parameter.getParameterAnnotation(Header.class);
|
||||
Assert.state(annot != null, "No Header annotation");
|
||||
return new HeaderNamedValueInfo(annot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, List<String> names)
|
||||
throws Exception {
|
||||
if (names.size() == 1) {
|
||||
return message.getHeaders().get(ARGUMENT_PREFIX + names.get(0));
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMissingValue(List<String> headerName, MethodParameter parameter, Message<?> message) {
|
||||
throw new MessageHandlingException(message, "Missing header '" + headerName +
|
||||
"' for method parameter type [" + parameter.getParameterType() + "]");
|
||||
}
|
||||
|
||||
private static final class HeaderNamedValueInfo extends NamedValueInfo {
|
||||
|
||||
private HeaderNamedValueInfo(Header annotation) {
|
||||
super(Arrays.asList(annotation.name()), annotation.required(), annotation.defaultValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.context.ShellContext;
|
||||
|
||||
/**
|
||||
* Interface defining contract to handle existing {@link CommandRegistration}s.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandCatalog {
|
||||
|
||||
/**
|
||||
* Register a {@link CommandRegistration}.
|
||||
*
|
||||
* @param registration the command registration
|
||||
*/
|
||||
void register(CommandRegistration... registration);
|
||||
|
||||
/**
|
||||
* Unregister a {@link CommandRegistration}.
|
||||
*
|
||||
* @param registration the command registration
|
||||
*/
|
||||
void unregister(CommandRegistration... registration);
|
||||
|
||||
/**
|
||||
* Unregister a {@link CommandRegistration} by its command name.
|
||||
*
|
||||
* @param commandName the command name
|
||||
*/
|
||||
void unregister(String... commandName);
|
||||
|
||||
/**
|
||||
* Gets all {@link CommandRegistration}s mapped with their names.
|
||||
* Returned map is a copy and cannot be used to register new commands.
|
||||
*
|
||||
* @return all command registrations
|
||||
*/
|
||||
Map<String, CommandRegistration> getRegistrations();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandCatalog}.
|
||||
*
|
||||
* @return default command catalog
|
||||
*/
|
||||
static CommandCatalog of() {
|
||||
return new DefaultCommandCatalog(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandCatalog}.
|
||||
*
|
||||
* @param resolvers the command resolvers
|
||||
* @param shellContext the shell context
|
||||
* @return default command catalog
|
||||
*/
|
||||
static CommandCatalog of(Collection<CommandResolver> resolvers, ShellContext shellContext) {
|
||||
return new DefaultCommandCatalog(resolvers, shellContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandCatalog}.
|
||||
*/
|
||||
static class DefaultCommandCatalog implements CommandCatalog {
|
||||
|
||||
private final Map<String, CommandRegistration> commandRegistrations = new HashMap<>();
|
||||
private final Collection<CommandResolver> resolvers = new ArrayList<>();
|
||||
private final ShellContext shellContext;
|
||||
|
||||
DefaultCommandCatalog(Collection<CommandResolver> resolvers, ShellContext shellContext) {
|
||||
this.shellContext = shellContext;
|
||||
if (resolvers != null) {
|
||||
this.resolvers.addAll(resolvers);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(CommandRegistration... registration) {
|
||||
for (CommandRegistration r : registration) {
|
||||
String commandName = r.getCommand();
|
||||
commandRegistrations.put(commandName, r);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(CommandRegistration... registration) {
|
||||
for (CommandRegistration r : registration) {
|
||||
String commandName = r.getCommand();
|
||||
commandRegistrations.remove(commandName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(String... commandName) {
|
||||
for (String n : commandName) {
|
||||
commandRegistrations.remove(n);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, CommandRegistration> getRegistrations() {
|
||||
Map<String, CommandRegistration> regs = new HashMap<>();
|
||||
regs.putAll(commandRegistrations);
|
||||
for (CommandResolver resolver : resolvers) {
|
||||
resolver.resolve().stream().forEach(r -> {
|
||||
regs.put(r.getCommand(), r);
|
||||
});
|
||||
}
|
||||
return regs.entrySet().stream()
|
||||
.filter(filterByInteractionMode(shellContext))
|
||||
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter registration entries by currently set mode. Having it set to ALL or null
|
||||
* effectively disables filtering as as we only care if mode is set to interactive
|
||||
* or non-interactive.
|
||||
*/
|
||||
private static Predicate<Entry<String, CommandRegistration>> filterByInteractionMode(ShellContext shellContext) {
|
||||
return e -> {
|
||||
InteractionMode mim = e.getValue().getInteractionMode();
|
||||
InteractionMode cim = shellContext != null ? shellContext.getInteractionMode() : InteractionMode.ALL;
|
||||
if (mim == null || cim == null || mim == InteractionMode.ALL) {
|
||||
return true;
|
||||
}
|
||||
else if (mim == InteractionMode.INTERACTIVE) {
|
||||
return cim == InteractionMode.INTERACTIVE || cim == InteractionMode.ALL;
|
||||
}
|
||||
else if (mim == InteractionMode.NONINTERACTIVE) {
|
||||
return cim == InteractionMode.NONINTERACTIVE || cim == InteractionMode.ALL;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// private static String commandName(String[] commands) {
|
||||
// return Arrays.asList(commands).stream()
|
||||
// .flatMap(c -> Stream.of(c.split(" ")))
|
||||
// .filter(c -> StringUtils.hasText(c))
|
||||
// .map(c -> c.trim())
|
||||
// .collect(Collectors.joining(" "));
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
/**
|
||||
* Interface to customize a {@link CommandCatalog}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandCatalogCustomizer {
|
||||
|
||||
/**
|
||||
* Customize a command catalog.
|
||||
*
|
||||
* @param commandCatalog a command catalog
|
||||
*/
|
||||
void customize(CommandCatalog commandCatalog);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResult;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResults;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Interface containing information about current command execution.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandContext {
|
||||
|
||||
/**
|
||||
* Gets a raw args passed into a currently executing command.
|
||||
*
|
||||
* @return raw command arguments
|
||||
*/
|
||||
String[] getRawArgs();
|
||||
|
||||
/**
|
||||
* Gets if option has been mapped.
|
||||
*
|
||||
* @param name the option name
|
||||
* @return true if option has been mapped, false otherwise
|
||||
*/
|
||||
boolean hasMappedOption(String name);
|
||||
|
||||
/**
|
||||
* Gets a command option parser results.
|
||||
*
|
||||
* @return the command option parser results
|
||||
*/
|
||||
CommandParserResults getParserResults();
|
||||
|
||||
/**
|
||||
* Gets an mapped option value.
|
||||
*
|
||||
* @param <T> the type to map to
|
||||
* @param name the option name
|
||||
* @return mapped value
|
||||
*/
|
||||
<T> T getOptionValue(String name);
|
||||
|
||||
/**
|
||||
* Gets a terminal.
|
||||
*
|
||||
* @return a terminal
|
||||
*/
|
||||
Terminal getTerminal();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandContext}.
|
||||
*
|
||||
* @param args the arguments
|
||||
* @param results the results
|
||||
* @param terminal the terminal
|
||||
* @return a command context
|
||||
*/
|
||||
static CommandContext of(String[] args, CommandParserResults results, Terminal terminal) {
|
||||
return new DefaultCommandContext(args, results, terminal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandContext}.
|
||||
*/
|
||||
static class DefaultCommandContext implements CommandContext {
|
||||
|
||||
private String[] args;
|
||||
private CommandParserResults results;
|
||||
private Terminal terminal;
|
||||
|
||||
DefaultCommandContext(String[] args, CommandParserResults results, Terminal terminal) {
|
||||
this.args = args;
|
||||
this.results = results;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getRawArgs() {
|
||||
return args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMappedOption(String name) {
|
||||
return find(name).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandParserResults getParserResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getOptionValue(String name) {
|
||||
Optional<CommandParserResult> find = find(name);
|
||||
if (find.isPresent()) {
|
||||
return (T) find.get().value();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Terminal getTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
private Optional<CommandParserResult> find(String name) {
|
||||
return results.results().stream()
|
||||
.filter(r -> {
|
||||
Stream<String> l = Arrays.asList(r.option().getLongNames()).stream();
|
||||
Stream<String> s = Arrays.asList(r.option().getShortNames()).stream().map(n -> Character.toString(n));
|
||||
return Stream.concat(l, s)
|
||||
.filter(o -> ObjectUtils.nullSafeEquals(o, name))
|
||||
.findFirst()
|
||||
.isPresent();
|
||||
})
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
|
||||
/**
|
||||
* Implementation of a {@link HandlerMethodArgumentResolver} resolving
|
||||
* {@link CommandContext}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class CommandContextMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
public static final String HEADER_COMMAND_CONTEXT = "springShellCommandContext";
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
MethodParameter nestedParameter = parameter.nestedIfOptional();
|
||||
Class<?> paramType = nestedParameter.getNestedParameterType();
|
||||
return CommandContext.class.isAssignableFrom(paramType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message){
|
||||
CommandContext commandContext = message.getHeaders().get(HEADER_COMMAND_CONTEXT, CommandContext.class);
|
||||
return parameter.isOptional() ? Optional.ofNullable(commandContext) : commandContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.validation.Validator;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserException;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResults;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo.TargetType;
|
||||
import org.springframework.shell.command.invocation.InvocableShellMethod;
|
||||
import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite;
|
||||
|
||||
/**
|
||||
* Interface to evaluate a result from a command with an arguments.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandExecution {
|
||||
|
||||
/**
|
||||
* Evaluate a command with a given arguments.
|
||||
*
|
||||
* @param registration the command registration
|
||||
* @param args the command args
|
||||
* @return evaluated execution
|
||||
*/
|
||||
Object evaluate(CommandRegistration registration, String[] args);
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandExecution}.
|
||||
*
|
||||
* @param resolvers the handler method argument resolvers
|
||||
* @return default command execution
|
||||
*/
|
||||
public static CommandExecution of(List<? extends HandlerMethodArgumentResolver> resolvers) {
|
||||
return new DefaultCommandExecution(resolvers, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandExecution}.
|
||||
*
|
||||
* @param resolvers the handler method argument resolvers
|
||||
* @param validator the validator
|
||||
* @param terminal the terminal
|
||||
* @return default command execution
|
||||
*/
|
||||
public static CommandExecution of(List<? extends HandlerMethodArgumentResolver> resolvers, Validator validator,
|
||||
Terminal terminal) {
|
||||
return new DefaultCommandExecution(resolvers, validator, terminal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandExecution}.
|
||||
*/
|
||||
static class DefaultCommandExecution implements CommandExecution {
|
||||
|
||||
private List<? extends HandlerMethodArgumentResolver> resolvers;
|
||||
private Validator validator;
|
||||
private Terminal terminal;
|
||||
|
||||
public DefaultCommandExecution(List<? extends HandlerMethodArgumentResolver> resolvers, Validator validator,
|
||||
Terminal terminal) {
|
||||
this.resolvers = resolvers;
|
||||
this.validator = validator;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
public Object evaluate(CommandRegistration registration, String[] args) {
|
||||
|
||||
List<CommandOption> options = registration.getOptions();
|
||||
CommandParser parser = CommandParser.of();
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
|
||||
if (!results.errors().isEmpty()) {
|
||||
throw new CommandParserExceptionsException("Command parser resulted errors", results.errors());
|
||||
}
|
||||
|
||||
CommandContext ctx = CommandContext.of(args, results, terminal);
|
||||
|
||||
Object res = null;
|
||||
|
||||
TargetInfo targetInfo = registration.getTarget();
|
||||
|
||||
// pick the target to execute
|
||||
if (targetInfo.getTargetType() == TargetType.FUNCTION) {
|
||||
res = targetInfo.getFunction().apply(ctx);
|
||||
}
|
||||
else if (targetInfo.getTargetType() == TargetType.CONSUMER) {
|
||||
targetInfo.getConsumer().accept(ctx);
|
||||
}
|
||||
else if (targetInfo.getTargetType() == TargetType.METHOD) {
|
||||
try {
|
||||
MessageBuilder<String[]> messageBuilder = MessageBuilder.withPayload(args);
|
||||
Map<String, Object> paramValues = new HashMap<>();
|
||||
results.results().stream().forEach(r -> {
|
||||
if (r.option().getLongNames() != null) {
|
||||
for (String n : r.option().getLongNames()) {
|
||||
messageBuilder.setHeader(ArgumentHeaderMethodArgumentResolver.ARGUMENT_PREFIX + n, r.value());
|
||||
paramValues.put(n, r.value());
|
||||
}
|
||||
}
|
||||
if (r.option().getShortNames() != null) {
|
||||
for (Character n : r.option().getShortNames()) {
|
||||
messageBuilder.setHeader(ArgumentHeaderMethodArgumentResolver.ARGUMENT_PREFIX + n.toString(), r.value());
|
||||
}
|
||||
}
|
||||
});
|
||||
messageBuilder.setHeader(CommandContextMethodArgumentResolver.HEADER_COMMAND_CONTEXT, ctx);
|
||||
|
||||
InvocableShellMethod invocableShellMethod = new InvocableShellMethod(targetInfo.getBean(), targetInfo.getMethod());
|
||||
invocableShellMethod.setValidator(validator);
|
||||
ShellMethodArgumentResolverComposite argumentResolvers = new ShellMethodArgumentResolverComposite();
|
||||
if (resolvers != null) {
|
||||
argumentResolvers.addResolvers(resolvers);
|
||||
}
|
||||
if (!paramValues.isEmpty()) {
|
||||
argumentResolvers.addResolver(new ParamNameHandlerMethodArgumentResolver(paramValues));
|
||||
}
|
||||
invocableShellMethod.setMessageMethodArgumentResolvers(argumentResolvers);
|
||||
|
||||
res = invocableShellMethod.invoke(messageBuilder.build(), (Object[])null);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new CommandExecutionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@Order(100)
|
||||
static class ParamNameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final Map<String, Object> paramValues = new HashMap<>();
|
||||
ConversionService conversionService = new DefaultConversionService();
|
||||
|
||||
ParamNameHandlerMethodArgumentResolver(Map<String, Object> paramValues) {
|
||||
this.paramValues.putAll(paramValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
String parameterName = parameter.getParameterName();
|
||||
if (parameterName == null) {
|
||||
return false;
|
||||
}
|
||||
return paramValues.containsKey(parameterName) && conversionService
|
||||
.canConvert(paramValues.get(parameterName).getClass(), parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
return conversionService.convert(paramValues.get(parameter.getParameterName()), parameter.getParameterType());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CommandExecutionException extends RuntimeException {
|
||||
|
||||
public CommandExecutionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandParserExceptionsException extends RuntimeException {
|
||||
|
||||
private final List<CommandParserException> parserExceptions;
|
||||
|
||||
public CommandParserExceptionsException(String message, List<CommandParserException> parserExceptions) {
|
||||
super(message);
|
||||
this.parserExceptions = parserExceptions;
|
||||
}
|
||||
|
||||
public List<CommandParserException> getParserExceptions() {
|
||||
return parserExceptions;
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandExecutionHandlerMethodArgumentResolvers {
|
||||
|
||||
private final List<? extends HandlerMethodArgumentResolver> resolvers;
|
||||
|
||||
public CommandExecutionHandlerMethodArgumentResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) {
|
||||
this.resolvers = resolvers;
|
||||
}
|
||||
|
||||
public List<? extends HandlerMethodArgumentResolver> getResolvers() {
|
||||
return resolvers;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Interface representing an option in a command.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandOption {
|
||||
|
||||
/**
|
||||
* Gets a long names of an option.
|
||||
*
|
||||
* @return long names of an option
|
||||
*/
|
||||
String[] getLongNames();
|
||||
|
||||
/**
|
||||
* Gets a short names of an option.
|
||||
*
|
||||
* @return short names of an option
|
||||
*/
|
||||
Character[] getShortNames();
|
||||
|
||||
/**
|
||||
* Gets a description of an option.
|
||||
*
|
||||
* @return description of an option
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Gets a {@link ResolvableType} of an option.
|
||||
*
|
||||
* @return type of an option
|
||||
*/
|
||||
ResolvableType getType();
|
||||
|
||||
/**
|
||||
* Gets a flag if option is required.
|
||||
*
|
||||
* @return the required flag
|
||||
*/
|
||||
boolean isRequired();
|
||||
|
||||
/**
|
||||
* Gets a default value of an option.
|
||||
*
|
||||
* @return the default value
|
||||
*/
|
||||
String getDefaultValue();
|
||||
|
||||
/**
|
||||
* Gets a positional value.
|
||||
*
|
||||
* @return the positional value
|
||||
*/
|
||||
int getPosition();
|
||||
|
||||
/**
|
||||
* Gets a minimum arity.
|
||||
*
|
||||
* @return the minimum arity
|
||||
*/
|
||||
int getArityMin();
|
||||
|
||||
/**
|
||||
* Gets a maximum arity.
|
||||
*
|
||||
* @return the maximum arity
|
||||
*/
|
||||
int getArityMax();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandOption}.
|
||||
*
|
||||
* @param longNames the long names
|
||||
* @param shortNames the short names
|
||||
* @param description the description
|
||||
* @return default command option
|
||||
*/
|
||||
public static CommandOption of(String[] longNames, Character[] shortNames, String description) {
|
||||
return of(longNames, shortNames, description, null, false, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandOption}.
|
||||
*
|
||||
* @param longNames the long names
|
||||
* @param shortNames the short names
|
||||
* @param description the description
|
||||
* @param type the type
|
||||
* @return default command option
|
||||
*/
|
||||
public static CommandOption of(String[] longNames, Character[] shortNames, String description,
|
||||
ResolvableType type) {
|
||||
return of(longNames, shortNames, description, type, false, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandOption}.
|
||||
*
|
||||
* @param longNames the long names
|
||||
* @param shortNames the short names
|
||||
* @param description the description
|
||||
* @param type the type
|
||||
* @param required the required flag
|
||||
* @param defaultValue the default value
|
||||
* @param position the position value
|
||||
* @param arityMin the min arity
|
||||
* @param arityMax the max arity
|
||||
* @return default command option
|
||||
*/
|
||||
public static CommandOption of(String[] longNames, Character[] shortNames, String description,
|
||||
ResolvableType type, boolean required, String defaultValue, Integer position, Integer arityMin, Integer arityMax) {
|
||||
return new DefaultCommandOption(longNames, shortNames, description, type, required, defaultValue, position,
|
||||
arityMin, arityMax);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link CommandOption}.
|
||||
*/
|
||||
public static class DefaultCommandOption implements CommandOption {
|
||||
|
||||
private String[] longNames;
|
||||
private Character[] shortNames;
|
||||
private String description;
|
||||
private ResolvableType type;
|
||||
private boolean required;
|
||||
private String defaultValue;
|
||||
private int position;
|
||||
private int arityMin;
|
||||
private int arityMax;
|
||||
|
||||
public DefaultCommandOption(String[] longNames, Character[] shortNames, String description,
|
||||
ResolvableType type, boolean required, String defaultValue, Integer position,
|
||||
Integer arityMin, Integer arityMax) {
|
||||
this.longNames = longNames != null ? longNames : new String[0];
|
||||
this.shortNames = shortNames != null ? shortNames : new Character[0];
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
this.required = required;
|
||||
this.defaultValue = defaultValue;
|
||||
this.position = position != null && position > -1 ? position : -1 ;
|
||||
this.arityMin = arityMin != null ? arityMin : -1;
|
||||
this.arityMax = arityMax != null ? arityMax : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getLongNames() {
|
||||
return longNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Character[] getShortNames() {
|
||||
return shortNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getArityMin() {
|
||||
return arityMin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getArityMax() {
|
||||
return arityMax;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Interface parsing arguments for a {@link CommandRegistration}. A command is
|
||||
* always identified by a set of words like
|
||||
* {@code command subcommand1 subcommand2} and remaining part of it are options
|
||||
* which this interface intercepts and translates into format we can understand.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandParser {
|
||||
|
||||
/**
|
||||
* Result of a parsing {@link CommandOption} with an argument.
|
||||
*/
|
||||
interface CommandParserResult {
|
||||
|
||||
/**
|
||||
* Gets the {@link CommandOption}.
|
||||
*
|
||||
* @return the command option
|
||||
*/
|
||||
CommandOption option();
|
||||
|
||||
/**
|
||||
* Gets the value.
|
||||
*
|
||||
* @return the value
|
||||
*/
|
||||
Object value();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandParserResult}.
|
||||
*
|
||||
* @param option the command option
|
||||
* @param value the value
|
||||
* @return a result
|
||||
*/
|
||||
static CommandParserResult of(CommandOption option, Object value) {
|
||||
return new DefaultCommandParserResult(option, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Results of a {@link CommandParser}. Basically contains a list of {@link CommandParserResult}s.
|
||||
*/
|
||||
interface CommandParserResults {
|
||||
|
||||
/**
|
||||
* Gets the results.
|
||||
*
|
||||
* @return the results
|
||||
*/
|
||||
List<CommandParserResult> results();
|
||||
|
||||
/**
|
||||
* Gets the unmapped positional arguments.
|
||||
*
|
||||
* @return the unmapped positional arguments
|
||||
*/
|
||||
List<String> positional();
|
||||
|
||||
/**
|
||||
* Gets parsing errors.
|
||||
*
|
||||
* @return the parsing errors
|
||||
*/
|
||||
List<CommandParserException> errors();
|
||||
|
||||
/**
|
||||
* Gets an instance of a default {@link CommandParserResults}.
|
||||
*
|
||||
* @param results the results
|
||||
* @param positional the list of positional arguments
|
||||
* @param errors the parsing errors
|
||||
* @return a new instance of results
|
||||
*/
|
||||
static CommandParserResults of(List<CommandParserResult> results, List<String> positional, List<CommandParserException> errors) {
|
||||
return new DefaultCommandParserResults(results, positional, errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse options with a given arguments.
|
||||
*
|
||||
* May throw various runtime exceptions depending how parser is configure.
|
||||
* For example if required option is missing an exception is thrown.
|
||||
*
|
||||
* @param options the command options
|
||||
* @param args the arguments
|
||||
* @return parsed results
|
||||
*/
|
||||
CommandParserResults parse(List<CommandOption> options, String[] args);
|
||||
|
||||
/**
|
||||
* Gets an instance of a default command parser.
|
||||
*
|
||||
* @return instance of a default command parser
|
||||
*/
|
||||
static CommandParser of() {
|
||||
return new DefaultCommandParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandParserResults}.
|
||||
*/
|
||||
static class DefaultCommandParserResults implements CommandParserResults {
|
||||
|
||||
private List<CommandParserResult> results;
|
||||
private List<String> positional;
|
||||
private List<CommandParserException> errors;
|
||||
|
||||
DefaultCommandParserResults(List<CommandParserResult> results, List<String> positional, List<CommandParserException> errors) {
|
||||
this.results = results;
|
||||
this.positional = positional;
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandParserResult> results() {
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> positional() {
|
||||
return positional;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandParserException> errors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandParserResult}.
|
||||
*/
|
||||
static class DefaultCommandParserResult implements CommandParserResult {
|
||||
|
||||
private CommandOption option;
|
||||
private Object value;
|
||||
|
||||
DefaultCommandParserResult(CommandOption option, Object value) {
|
||||
this.option = option;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandOption option() {
|
||||
return option;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CommandParser}.
|
||||
*/
|
||||
static class DefaultCommandParser implements CommandParser {
|
||||
|
||||
@Override
|
||||
public CommandParserResults parse(List<CommandOption> options, String[] args) {
|
||||
List<CommandOption> requiredOptions = options.stream()
|
||||
.filter(o -> o.isRequired())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Lexer lexer = new Lexer(args);
|
||||
List<List<String>> lexerResults = lexer.visit();
|
||||
Parser parser = new Parser();
|
||||
ParserResults parserResults = parser.visit(lexerResults, options);
|
||||
|
||||
List<CommandParserResult> results = new ArrayList<>();
|
||||
List<String> positional = new ArrayList<>();
|
||||
List<CommandParserException> errors = new ArrayList<>();
|
||||
parserResults.results.stream().forEach(pr -> {
|
||||
if (pr.option != null) {
|
||||
results.add(new DefaultCommandParserResult(pr.option, pr.value));
|
||||
requiredOptions.remove(pr.option);
|
||||
}
|
||||
else {
|
||||
positional.addAll(pr.args);
|
||||
}
|
||||
if (pr.error != null) {
|
||||
errors.add(pr.error);
|
||||
}
|
||||
});
|
||||
|
||||
Deque<ParserResult> queue = new ArrayDeque<>(parserResults.results);
|
||||
options.stream()
|
||||
.filter(o -> o.getPosition() > -1)
|
||||
.sorted(Comparator.comparingInt(o -> o.getPosition()))
|
||||
.forEach(o -> {
|
||||
int arityMin = o.getArityMin();
|
||||
int arityMax = o.getArityMax();
|
||||
List<String> oargs = new ArrayList<>();
|
||||
if (arityMin > -1) {
|
||||
for (int i = 0; i < arityMax; i++) {
|
||||
ParserResult pop = null;
|
||||
if (!queue.isEmpty()) {
|
||||
pop = queue.pop();
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
if (pop != null && pop.option == null) {
|
||||
if (!pop.args.isEmpty()) {
|
||||
oargs.add(pop.args.stream().collect(Collectors.joining(" ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!oargs.isEmpty()) {
|
||||
results.add(new DefaultCommandParserResult(o, oargs.stream().collect(Collectors.joining(" "))));
|
||||
requiredOptions.remove(o);
|
||||
}
|
||||
});
|
||||
|
||||
requiredOptions.stream().forEach(o -> {
|
||||
String ln = o.getLongNames() != null ? Stream.of(o.getLongNames()).collect(Collectors.joining(",")) : "";
|
||||
String sn = o.getShortNames() != null ? Stream.of(o.getShortNames()).map(n -> Character.toString(n))
|
||||
.collect(Collectors.joining(",")) : "";
|
||||
errors.add(MissingOptionException
|
||||
.of(String.format("Missing option, longnames='%s', shortnames='%s'", ln, sn), o));
|
||||
});
|
||||
|
||||
return new DefaultCommandParserResults(results, positional, errors);
|
||||
}
|
||||
|
||||
private static class ParserResult {
|
||||
private CommandOption option;
|
||||
private List<String> args;
|
||||
private Object value;
|
||||
private CommandParserException error;
|
||||
|
||||
private ParserResult(CommandOption option, List<String> args, Object value, CommandParserException error) {
|
||||
this.option = option;
|
||||
this.args = args;
|
||||
this.value = value;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
static ParserResult of(CommandOption option, List<String> args, Object value,
|
||||
CommandParserException error) {
|
||||
return new ParserResult(option, args, value, error);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParserResults {
|
||||
private List<ParserResult> results;
|
||||
|
||||
private ParserResults(List<ParserResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
static ParserResults of(List<ParserResult> results) {
|
||||
return new ParserResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser works on a results from a lexer. It looks for given options
|
||||
* and builds parsing results.
|
||||
*/
|
||||
private static class Parser {
|
||||
ParserResults visit(List<List<String>> lexerResults, List<CommandOption> options) {
|
||||
List<ParserResult> results = lexerResults.stream()
|
||||
.flatMap(lr -> {
|
||||
List<CommandOption> option = matchOptions(options, lr.get(0));
|
||||
if (option.isEmpty()) {
|
||||
return lr.stream().map(a -> ParserResult.of(null, Arrays.asList(a), null, null));
|
||||
}
|
||||
else {
|
||||
return option.stream().flatMap(o -> {
|
||||
List<String> subArgs = lr.subList(1, lr.size());
|
||||
ConvertArgumentsHolder holder = convertArguments(o, subArgs);
|
||||
Object value = holder.value;
|
||||
Stream<ParserResult> unmapped = holder.unmapped.stream()
|
||||
.map(um -> ParserResult.of(null, Arrays.asList(um), null, null));
|
||||
Stream<ParserResult> res = Stream.of(ParserResult.of(o, subArgs, value, null));
|
||||
return Stream.concat(res, unmapped);
|
||||
});
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return ParserResults.of(results);
|
||||
}
|
||||
|
||||
private List<CommandOption> matchOptions(List<CommandOption> options, String arg) {
|
||||
List<CommandOption> matched = new ArrayList<>();
|
||||
String trimmed = StringUtils.trimLeadingCharacter(arg, '-');
|
||||
int count = arg.length() - trimmed.length();
|
||||
if (count == 1) {
|
||||
if (trimmed.length() == 1) {
|
||||
Character trimmedChar = trimmed.charAt(0);
|
||||
options.stream()
|
||||
.filter(o -> {
|
||||
for (Character sn : o.getShortNames()) {
|
||||
if (trimmedChar.equals(sn)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(o -> matched.add(o));
|
||||
}
|
||||
else if (trimmed.length() > 1) {
|
||||
trimmed.chars().mapToObj(i -> (char)i)
|
||||
.forEach(c -> {
|
||||
options.stream().forEach(o -> {
|
||||
for (Character sn : o.getShortNames()) {
|
||||
if (c.equals(sn)) {
|
||||
matched.add(o);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (count == 2) {
|
||||
options.stream()
|
||||
.filter(o -> {
|
||||
for (String ln : o.getLongNames()) {
|
||||
if (trimmed.equals(ln)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(o -> matched.add(o));
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private ConvertArgumentsHolder convertArguments(CommandOption option, List<String> arguments) {
|
||||
Object value = null;
|
||||
List<String> unmapped = new ArrayList<>();
|
||||
|
||||
ResolvableType type = option.getType();
|
||||
int arityMin = option.getArityMin();
|
||||
int arityMax = option.getArityMax();
|
||||
|
||||
if (arityMin < 0 && type != null) {
|
||||
if (type.isAssignableFrom(boolean.class)) {
|
||||
arityMin = 1;
|
||||
arityMax = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (type != null && type.isAssignableFrom(boolean.class)) {
|
||||
if (arguments.size() == 0) {
|
||||
value = true;
|
||||
}
|
||||
else {
|
||||
value = Boolean.parseBoolean(arguments.get(0));
|
||||
}
|
||||
}
|
||||
else if (type != null && type.isArray()) {
|
||||
value = arguments.stream().collect(Collectors.toList()).toArray();
|
||||
}
|
||||
else {
|
||||
if (!arguments.isEmpty()) {
|
||||
if (arguments.size() == 1) {
|
||||
value = arguments.get(0);
|
||||
}
|
||||
else {
|
||||
if (arityMax > 0) {
|
||||
int limit = Math.min(arguments.size(), arityMax);
|
||||
value = arguments.stream().limit(limit).collect(Collectors.joining(" "));
|
||||
unmapped.addAll(arguments.subList(limit, arguments.size()));
|
||||
}
|
||||
else {
|
||||
value = arguments.get(0);
|
||||
unmapped.addAll(arguments.subList(1, arguments.size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConvertArgumentsHolder.of(value, unmapped);
|
||||
}
|
||||
|
||||
private static class ConvertArgumentsHolder {
|
||||
Object value;
|
||||
final List<String> unmapped = new ArrayList<>();
|
||||
|
||||
ConvertArgumentsHolder(Object value, List<String> unmapped) {
|
||||
this.value = value;
|
||||
if (unmapped != null) {
|
||||
this.unmapped.addAll(unmapped);
|
||||
}
|
||||
}
|
||||
|
||||
static ConvertArgumentsHolder of(Object value, List<String> unmapped) {
|
||||
return new ConvertArgumentsHolder(value, unmapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexers only responsibility is to splice arguments array into
|
||||
* chunks which belongs together what comes for option structure.
|
||||
*/
|
||||
private static class Lexer {
|
||||
private final String[] args;
|
||||
Lexer(String[] args) {
|
||||
this.args = args;
|
||||
}
|
||||
List<List<String>> visit() {
|
||||
return Utils.split(args, t -> t.startsWith("-"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class CommandParserException extends RuntimeException {
|
||||
|
||||
public CommandParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CommandParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public static CommandParserException of(String message) {
|
||||
return new CommandParserException(message);
|
||||
}
|
||||
}
|
||||
|
||||
static class MissingOptionException extends CommandParserException {
|
||||
|
||||
private CommandOption option;
|
||||
|
||||
public MissingOptionException(String message, CommandOption option) {
|
||||
super(message);
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
public static MissingOptionException of(String message, CommandOption option) {
|
||||
return new MissingOptionException(message, option);
|
||||
}
|
||||
|
||||
public CommandOption getOption() {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Interface defining a command registration endpoint.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CommandRegistration {
|
||||
|
||||
/**
|
||||
* Gets a command for this registration.
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
String getCommand();
|
||||
|
||||
/**
|
||||
* Gets an {@link InteractionMode}.
|
||||
*
|
||||
* @return the interaction mode
|
||||
*/
|
||||
InteractionMode getInteractionMode();
|
||||
|
||||
/**
|
||||
* Get help for a command.
|
||||
*
|
||||
* @return the help
|
||||
*/
|
||||
String getHelp();
|
||||
|
||||
/**
|
||||
* Get group for a command.
|
||||
*
|
||||
* @return the group
|
||||
*/
|
||||
String getGroup();
|
||||
|
||||
/**
|
||||
* Get description for a command.
|
||||
*
|
||||
* @return the description
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Get {@link Availability} for a command
|
||||
*
|
||||
* @return the availability
|
||||
*/
|
||||
Availability getAvailability();
|
||||
|
||||
/**
|
||||
* Gets target info.
|
||||
*
|
||||
* @return the target info
|
||||
*/
|
||||
TargetInfo getTarget();
|
||||
|
||||
/**
|
||||
* Gets an options.
|
||||
*
|
||||
* @return the options
|
||||
*/
|
||||
List<CommandOption> getOptions();
|
||||
|
||||
/**
|
||||
* Gets a new instance of a {@link Buidler}.
|
||||
*
|
||||
* @return a new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new DefaultBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec defining an option.
|
||||
*/
|
||||
public interface OptionSpec {
|
||||
|
||||
/**
|
||||
* Define long option names.
|
||||
*
|
||||
* @param names the long option names
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec longNames(String... names);
|
||||
|
||||
/**
|
||||
* Define short option names.
|
||||
*
|
||||
* @param names the long option names
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec shortNames(Character... names);
|
||||
|
||||
/**
|
||||
* Define a type for an option.
|
||||
*
|
||||
* @param type the type
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec type(Type type);
|
||||
|
||||
/**
|
||||
* Define a {@code description} for an option.
|
||||
*
|
||||
* @param description the option description
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec description(String description);
|
||||
|
||||
/**
|
||||
* Define if option is required.
|
||||
*
|
||||
* @param required the required flag
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec required(boolean required);
|
||||
|
||||
/**
|
||||
* Define option to be required. Syntatic sugar calling
|
||||
* {@link #required(boolean)} with {@code true}.
|
||||
*
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec required();
|
||||
|
||||
/**
|
||||
* Define a {@code defaultValue} for an option.
|
||||
*
|
||||
* @param defaultValue the option default value
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec defaultValue(String defaultValue);
|
||||
|
||||
/**
|
||||
* Define an optional hint for possible positional mapping.
|
||||
*
|
||||
* @param position the position
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec position(Integer position);
|
||||
|
||||
/**
|
||||
* Define an {@code arity} for an option.
|
||||
*
|
||||
* @param min the min arity
|
||||
* @param max the max arity
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec arity(int min, int max);
|
||||
|
||||
/**
|
||||
* Define an {@code arity} for an option.
|
||||
*
|
||||
* @param arity the arity
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec arity(OptionArity arity);
|
||||
|
||||
/**
|
||||
* Return a builder for chaining.
|
||||
*
|
||||
* @return a builder for chaining
|
||||
*/
|
||||
Builder and();
|
||||
}
|
||||
|
||||
public enum OptionArity {
|
||||
ZERO,
|
||||
ZERO_OR_ONE,
|
||||
EXACTLY_ONE,
|
||||
ZERO_OR_MORE,
|
||||
ONE_OR_MORE
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates info for {@link TargetSpec}.
|
||||
*/
|
||||
public interface TargetInfo {
|
||||
|
||||
/**
|
||||
* Get target type
|
||||
*
|
||||
* @return the target type
|
||||
*/
|
||||
TargetType getTargetType();
|
||||
|
||||
/**
|
||||
* Get the bean.
|
||||
*
|
||||
* @return the bean
|
||||
*/
|
||||
Object getBean();
|
||||
|
||||
/**
|
||||
* Get the bean method
|
||||
*
|
||||
* @return the bean method
|
||||
*/
|
||||
Method getMethod();
|
||||
|
||||
/**
|
||||
* Get the function
|
||||
*
|
||||
* @return the function
|
||||
*/
|
||||
Function<CommandContext, ?> getFunction();
|
||||
|
||||
/**
|
||||
* Get the consumer
|
||||
*
|
||||
* @return the consumer
|
||||
*/
|
||||
Consumer<CommandContext> getConsumer();
|
||||
|
||||
static TargetInfo of(Object bean, Method method) {
|
||||
return new DefaultTargetInfo(TargetType.METHOD, bean, method, null, null);
|
||||
}
|
||||
|
||||
static TargetInfo of(Function<CommandContext, ?> function) {
|
||||
return new DefaultTargetInfo(TargetType.FUNCTION, null, null, function, null);
|
||||
}
|
||||
|
||||
static TargetInfo of(Consumer<CommandContext> consumer) {
|
||||
return new DefaultTargetInfo(TargetType.CONSUMER, null, null, null, consumer);
|
||||
}
|
||||
|
||||
enum TargetType {
|
||||
METHOD, FUNCTION, CONSUMER;
|
||||
}
|
||||
|
||||
static class DefaultTargetInfo implements TargetInfo {
|
||||
|
||||
private final TargetType targetType;
|
||||
private final Object bean;
|
||||
private final Method method;
|
||||
private final Function<CommandContext, ?> function;
|
||||
private final Consumer<CommandContext> consumer;
|
||||
|
||||
public DefaultTargetInfo(TargetType targetType, Object bean, Method method,
|
||||
Function<CommandContext, ?> function, Consumer<CommandContext> consumer) {
|
||||
this.targetType = targetType;
|
||||
this.bean = bean;
|
||||
this.method = method;
|
||||
this.function = function;
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetType getTargetType() {
|
||||
return targetType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<CommandContext, ?> getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consumer<CommandContext> getConsumer() {
|
||||
return consumer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec defining a target.
|
||||
*/
|
||||
public interface TargetSpec {
|
||||
|
||||
/**
|
||||
* Register a method target.
|
||||
*
|
||||
* @param bean the bean
|
||||
* @param method the method
|
||||
* @param paramTypes the parameter types
|
||||
* @return a target spec for chaining
|
||||
*/
|
||||
TargetSpec method(Object bean, String method, @Nullable Class<?>... paramTypes);
|
||||
|
||||
/**
|
||||
* Register a method target.
|
||||
*
|
||||
* @param bean the bean
|
||||
* @param method the method
|
||||
* @return a target spec for chaining
|
||||
*/
|
||||
TargetSpec method(Object bean, Method method);
|
||||
|
||||
/**
|
||||
* Register a function target.
|
||||
*
|
||||
* @param function the function to register
|
||||
* @return a target spec for chaining
|
||||
*/
|
||||
TargetSpec function(Function<CommandContext, ?> function);
|
||||
|
||||
/**
|
||||
* Register a consumer target.
|
||||
*
|
||||
* @param consumer the consumer to register
|
||||
* @return a target spec for chaining
|
||||
*/
|
||||
TargetSpec consumer(Consumer<CommandContext> consumer);
|
||||
|
||||
/**
|
||||
* Return a builder for chaining.
|
||||
*
|
||||
* @return a builder for chaining
|
||||
*/
|
||||
Builder and();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder interface for {@link CommandRegistration}.
|
||||
*/
|
||||
public interface Builder {
|
||||
|
||||
/**
|
||||
* Define commands this registration uses. Essentially defines a full set of
|
||||
* main and sub commands. It doesn't matter if full command is defined in one
|
||||
* string or multiple strings as "words" are splitted and trimmed with
|
||||
* whitespaces. You will get result of {@code command subcommand1 subcommand2, ...}.
|
||||
*
|
||||
* @param commands the commands
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder command(String... commands);
|
||||
|
||||
/**
|
||||
* Define {@link InteractionMode} for a command.
|
||||
*
|
||||
* @param mode the interaction mode
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder interactionMode(InteractionMode mode);
|
||||
|
||||
/**
|
||||
* Define a simple help text for a command.
|
||||
*
|
||||
* @param help the help text
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder help(String help);
|
||||
|
||||
/**
|
||||
* Define an {@link Availability} suppliear for a command.
|
||||
*
|
||||
* @param availability the availability
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder availability(Supplier<Availability> availability);
|
||||
|
||||
/**
|
||||
* Define a group for a command.
|
||||
*
|
||||
* @param group the group
|
||||
* @return builder for chaining
|
||||
*/
|
||||
Builder group(String group);
|
||||
|
||||
/**
|
||||
* Define an option what this command should user for. Can be used multiple
|
||||
* times.
|
||||
*
|
||||
* @return option spec for chaining
|
||||
*/
|
||||
OptionSpec withOption();
|
||||
|
||||
/**
|
||||
* Define a target what this command should execute
|
||||
*
|
||||
* @return target spec for chaining
|
||||
*/
|
||||
TargetSpec withTarget();
|
||||
|
||||
/**
|
||||
* Builds a {@link CommandRegistration}.
|
||||
*
|
||||
* @return a command registration
|
||||
*/
|
||||
CommandRegistration build();
|
||||
}
|
||||
|
||||
static class DefaultOptionSpec implements OptionSpec {
|
||||
|
||||
private BaseBuilder builder;
|
||||
private String[] longNames;
|
||||
private Character[] shortNames;
|
||||
private ResolvableType type;
|
||||
private String description;
|
||||
private boolean required;
|
||||
private String defaultValue;
|
||||
private Integer position;
|
||||
private Integer arityMin;
|
||||
private Integer arityMax;
|
||||
|
||||
DefaultOptionSpec(BaseBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec longNames(String... names) {
|
||||
this.longNames = names;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec shortNames(Character... names) {
|
||||
this.shortNames = names;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec type(Type type) {
|
||||
this.type = ResolvableType.forType(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec required(boolean required) {
|
||||
this.required = required;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec required() {
|
||||
return required(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec defaultValue(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec position(Integer position) {
|
||||
this.position = position;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec arity(int min, int max) {
|
||||
Assert.isTrue(min > -1, "arity min must be 0 or more");
|
||||
Assert.isTrue(max >= min, "arity max must be equal more than min");
|
||||
this.arityMin = min;
|
||||
this.arityMax = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec arity(OptionArity arity) {
|
||||
switch (arity) {
|
||||
case ZERO:
|
||||
this.arityMin = 0;
|
||||
this.arityMax = 0;
|
||||
break;
|
||||
case ZERO_OR_ONE:
|
||||
this.arityMin = 0;
|
||||
this.arityMax = Integer.MAX_VALUE;
|
||||
break;
|
||||
case EXACTLY_ONE:
|
||||
this.arityMin = 1;
|
||||
this.arityMax = 1;
|
||||
break;
|
||||
case ZERO_OR_MORE:
|
||||
this.arityMin = 0;
|
||||
this.arityMax = Integer.MAX_VALUE;
|
||||
break;
|
||||
case ONE_OR_MORE:
|
||||
this.arityMin = 1;
|
||||
this.arityMax = Integer.MAX_VALUE;
|
||||
break;
|
||||
default:
|
||||
this.arityMin = 0;
|
||||
this.arityMax = 0;
|
||||
break;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder and() {
|
||||
return builder;
|
||||
}
|
||||
|
||||
public String[] getLongNames() {
|
||||
return longNames;
|
||||
}
|
||||
|
||||
public Character[] getShortNames() {
|
||||
return shortNames;
|
||||
}
|
||||
|
||||
public ResolvableType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Integer getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public Integer getArityMin() {
|
||||
return arityMin;
|
||||
}
|
||||
|
||||
public Integer getArityMax() {
|
||||
return arityMax;
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultTargetSpec implements TargetSpec {
|
||||
|
||||
private BaseBuilder builder;
|
||||
private Object bean;
|
||||
private Method method;
|
||||
private Function<CommandContext, ?> function;
|
||||
private Consumer<CommandContext> consumer;
|
||||
|
||||
DefaultTargetSpec(BaseBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec method(Object bean, Method method) {
|
||||
this.bean = bean;
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec method(Object bean, String method, Class<?>... paramTypes) {
|
||||
this.bean = bean;
|
||||
this.method = ReflectionUtils.findMethod(bean.getClass(), method,
|
||||
ObjectUtils.isEmpty(paramTypes) ? null : paramTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec function(Function<CommandContext, ?> function) {
|
||||
this.function = function;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec consumer(Consumer<CommandContext> consumer) {
|
||||
this.consumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder and() {
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultCommandRegistration implements CommandRegistration {
|
||||
|
||||
private String command;
|
||||
private InteractionMode interactionMode;
|
||||
private String help;
|
||||
private String group;
|
||||
private String description;
|
||||
private Supplier<Availability> availability;
|
||||
private List<DefaultOptionSpec> optionSpecs;
|
||||
private DefaultTargetSpec targetSpec;
|
||||
|
||||
public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String help,
|
||||
String group, String description, Supplier<Availability> availability,
|
||||
List<DefaultOptionSpec> optionSpecs, DefaultTargetSpec targetSpec) {
|
||||
this.command = commandArrayToName(commands);
|
||||
this.interactionMode = interactionMode;
|
||||
this.help = help;
|
||||
this.group = group;
|
||||
this.description = description;
|
||||
this.availability = availability;
|
||||
this.optionSpecs = optionSpecs;
|
||||
this.targetSpec = targetSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionMode getInteractionMode() {
|
||||
return interactionMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelp() {
|
||||
return help;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Availability getAvailability() {
|
||||
return availability != null ? availability.get() : Availability.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandOption> getOptions() {
|
||||
return optionSpecs.stream()
|
||||
.map(o -> CommandOption.of(o.getLongNames(), o.getShortNames(), o.getDescription(), o.getType(),
|
||||
o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetInfo getTarget() {
|
||||
if (targetSpec.bean != null) {
|
||||
return TargetInfo.of(targetSpec.bean, targetSpec.method);
|
||||
}
|
||||
if (targetSpec.function != null) {
|
||||
return TargetInfo.of(targetSpec.function);
|
||||
}
|
||||
if (targetSpec.consumer != null) {
|
||||
return TargetInfo.of(targetSpec.consumer);
|
||||
}
|
||||
throw new IllegalArgumentException("No bean, function or consumer defined");
|
||||
}
|
||||
|
||||
private static String commandArrayToName(String[] commands) {
|
||||
return Arrays.asList(commands).stream()
|
||||
.flatMap(c -> Stream.of(c.split(" ")))
|
||||
.filter(c -> StringUtils.hasText(c))
|
||||
.map(c -> c.trim())
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
}
|
||||
|
||||
static class DefaultBuilder extends BaseBuilder {
|
||||
|
||||
}
|
||||
|
||||
static class BaseBuilder implements Builder {
|
||||
|
||||
private String[] commands;
|
||||
private InteractionMode interactionMode = InteractionMode.ALL;
|
||||
private String help;
|
||||
private String group;
|
||||
private String description;
|
||||
private Supplier<Availability> availability;
|
||||
private List<DefaultOptionSpec> optionSpecs = new ArrayList<>();
|
||||
private DefaultTargetSpec targetSpec;
|
||||
|
||||
@Override
|
||||
public Builder command(String... commands) {
|
||||
Assert.notNull(commands, "commands must be set");
|
||||
this.commands = Arrays.asList(commands).stream()
|
||||
.flatMap(c -> Stream.of(c.split(" ")))
|
||||
.filter(c -> StringUtils.hasText(c))
|
||||
.map(c -> c.trim())
|
||||
.collect(Collectors.toList())
|
||||
.toArray(new String[0]);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder interactionMode(InteractionMode mode) {
|
||||
this.interactionMode = mode != null ? mode : InteractionMode.ALL;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder help(String help) {
|
||||
this.help = help;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder group(String group) {
|
||||
this.group = group;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder availability(Supplier<Availability> availability) {
|
||||
this.availability = availability;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionSpec withOption() {
|
||||
DefaultOptionSpec spec = new DefaultOptionSpec(this);
|
||||
optionSpecs.add(spec);
|
||||
return spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetSpec withTarget() {
|
||||
DefaultTargetSpec spec = new DefaultTargetSpec(this);
|
||||
targetSpec = spec;
|
||||
return spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandRegistration build() {
|
||||
Assert.notNull(commands, "command cannot be empty");
|
||||
Assert.notNull(targetSpec, "target cannot be empty");
|
||||
Assert.state(!(targetSpec.bean != null && targetSpec.function != null), "only one target can exist");
|
||||
return new DefaultCommandRegistration(commands, interactionMode, help, group, description, availability,
|
||||
optionSpecs, targetSpec);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface to resolve currently existing commands. It is useful to have fully
|
||||
* dynamic set of commands which may exists only if some conditions in a running
|
||||
* shell are met. For example if shell is targeting arbitrary server environment
|
||||
* some commands may or may not exist depending on a runtime state.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandResolver {
|
||||
|
||||
/**
|
||||
* Resolve command registrations.
|
||||
*
|
||||
* @return command registrations
|
||||
*/
|
||||
List<CommandRegistration> resolve();
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
/*
|
||||
* 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.command.invocation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validator;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.annotation.SynthesizingMethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.shell.ParameterValidationException;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Encapsulates information about a handler method consisting of a
|
||||
* {@linkplain #getMethod() method} and a {@linkplain #getBean() bean}.
|
||||
* Provides convenient access to method parameters, the method return value,
|
||||
* method annotations, etc.
|
||||
*
|
||||
* <p>The class may be created with a bean instance or with a bean name
|
||||
* (e.g. lazy-init bean, prototype bean). Use {@link #createWithResolvedBean()}
|
||||
* to obtain a {@code HandlerMethod} instance with a bean instance resolved
|
||||
* through the associated {@link BeanFactory}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class InvocableShellMethod {
|
||||
|
||||
/** Public for wrapping with fallback logger. */
|
||||
public static final Logger log = LoggerFactory.getLogger(InvocableShellMethod.class);
|
||||
|
||||
private static final Object[] EMPTY_ARGS = new Object[0];
|
||||
|
||||
private final Object bean;
|
||||
|
||||
@Nullable
|
||||
private final BeanFactory beanFactory;
|
||||
|
||||
private final Class<?> beanType;
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Method bridgedMethod;
|
||||
|
||||
private final MethodParameter[] parameters;
|
||||
|
||||
@Nullable
|
||||
private InvocableShellMethod resolvedFromHandlerMethod;
|
||||
|
||||
private ShellMethodArgumentResolverComposite resolvers = new ShellMethodArgumentResolverComposite();
|
||||
|
||||
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
private Validator validator;
|
||||
|
||||
/**
|
||||
* Create an instance from a bean instance and a method.
|
||||
*/
|
||||
public InvocableShellMethod(Object bean, Method method) {
|
||||
Assert.notNull(bean, "Bean is required");
|
||||
Assert.notNull(method, "Method is required");
|
||||
this.bean = bean;
|
||||
this.beanFactory = null;
|
||||
this.beanType = ClassUtils.getUserClass(bean);
|
||||
this.method = method;
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from a bean instance, method name, and parameter types.
|
||||
* @throws NoSuchMethodException when the method cannot be found
|
||||
*/
|
||||
public InvocableShellMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
|
||||
Assert.notNull(bean, "Bean is required");
|
||||
Assert.notNull(methodName, "Method name is required");
|
||||
this.bean = bean;
|
||||
this.beanFactory = null;
|
||||
this.beanType = ClassUtils.getUserClass(bean);
|
||||
this.method = bean.getClass().getMethod(methodName, parameterTypes);
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from a bean name, a method, and a {@code BeanFactory}.
|
||||
* The method {@link #createWithResolvedBean()} may be used later to
|
||||
* re-create the {@code HandlerMethod} with an initialized bean.
|
||||
*/
|
||||
public InvocableShellMethod(String beanName, BeanFactory beanFactory, Method method) {
|
||||
Assert.hasText(beanName, "Bean name is required");
|
||||
Assert.notNull(beanFactory, "BeanFactory is required");
|
||||
Assert.notNull(method, "Method is required");
|
||||
this.bean = beanName;
|
||||
this.beanFactory = beanFactory;
|
||||
Class<?> beanType = beanFactory.getType(beanName);
|
||||
if (beanType == null) {
|
||||
throw new IllegalStateException("Cannot resolve bean type for bean with name '" + beanName + "'");
|
||||
}
|
||||
this.beanType = ClassUtils.getUserClass(beanType);
|
||||
this.method = method;
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor for use in subclasses.
|
||||
*/
|
||||
protected InvocableShellMethod(InvocableShellMethod handlerMethod) {
|
||||
Assert.notNull(handlerMethod, "HandlerMethod is required");
|
||||
this.bean = handlerMethod.bean;
|
||||
this.beanFactory = handlerMethod.beanFactory;
|
||||
this.beanType = handlerMethod.beanType;
|
||||
this.method = handlerMethod.method;
|
||||
this.bridgedMethod = handlerMethod.bridgedMethod;
|
||||
this.parameters = handlerMethod.parameters;
|
||||
this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-create HandlerMethod with the resolved handler.
|
||||
*/
|
||||
private InvocableShellMethod(InvocableShellMethod handlerMethod, Object handler) {
|
||||
Assert.notNull(handlerMethod, "HandlerMethod is required");
|
||||
Assert.notNull(handler, "Handler object is required");
|
||||
this.bean = handler;
|
||||
this.beanFactory = handlerMethod.beanFactory;
|
||||
this.beanType = handlerMethod.beanType;
|
||||
this.method = handlerMethod.method;
|
||||
this.bridgedMethod = handlerMethod.bridgedMethod;
|
||||
this.parameters = handlerMethod.parameters;
|
||||
this.resolvedFromHandlerMethod = handlerMethod;
|
||||
}
|
||||
|
||||
public void setValidator(Validator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use to use for resolving method argument values.
|
||||
*/
|
||||
public void setMessageMethodArgumentResolvers(ShellMethodArgumentResolverComposite argumentResolvers) {
|
||||
this.resolvers = argumentResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ParameterNameDiscoverer for resolving parameter names when needed
|
||||
* (e.g. default request attribute name).
|
||||
* <p>Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}.
|
||||
*/
|
||||
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
this.parameterNameDiscoverer = parameterNameDiscoverer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the method after resolving its argument values in the context of the given message.
|
||||
* <p>Argument values are commonly resolved through
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
* The {@code providedArgs} parameter however may supply argument values to be used directly,
|
||||
* i.e. without argument resolution.
|
||||
* <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
|
||||
* resolved arguments.
|
||||
* @param message the current message being processed
|
||||
* @param providedArgs "given" arguments matched by type, not resolved
|
||||
* @return the raw value returned by the invoked method
|
||||
* @throws Exception raised if no suitable argument resolver can be found,
|
||||
* or if the method raised an exception
|
||||
* @see #getMethodArgumentValues
|
||||
* @see #doInvoke
|
||||
*/
|
||||
@Nullable
|
||||
public Object invoke(Message<?> message, Object... providedArgs) throws Exception {
|
||||
Object[] args = getMethodArgumentValues(message, providedArgs);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Arguments: " + Arrays.toString(args));
|
||||
}
|
||||
return doInvoke(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method argument values for the current message, checking the provided
|
||||
* argument values and falling back to the configured argument resolvers.
|
||||
* <p>The resulting array will be passed into {@link #doInvoke}.
|
||||
*/
|
||||
protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
|
||||
ConversionService conversionService = new DefaultConversionService();
|
||||
MethodParameter[] parameters = getMethodParameters();
|
||||
if (ObjectUtils.isEmpty(parameters)) {
|
||||
return EMPTY_ARGS;
|
||||
}
|
||||
|
||||
ResolvedHolder[] holders = new ResolvedHolder[parameters.length];
|
||||
|
||||
int unresolvedCount = 0;
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
MethodParameter parameter = parameters[i];
|
||||
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
||||
boolean supports = this.resolvers.supportsParameter(parameter);
|
||||
Object arg = null;
|
||||
if (supports) {
|
||||
arg = this.resolvers.resolveArgument(parameter, message);
|
||||
}
|
||||
else {
|
||||
unresolvedCount++;
|
||||
}
|
||||
holders[i] = new ResolvedHolder(supports, parameter, arg);
|
||||
}
|
||||
|
||||
Object[] args = new Object[parameters.length];
|
||||
int providedArgsIndex = 0;
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
if (!holders[i].resolved) {
|
||||
if (providedArgs != null && unresolvedCount <= providedArgs.length) {
|
||||
if (conversionService.canConvert(providedArgs[providedArgsIndex].getClass(), holders[i].parameter.getParameterType())) {
|
||||
holders[i].arg = conversionService.convert(providedArgs[providedArgsIndex], holders[i].parameter.getParameterType());
|
||||
providedArgsIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
args[i] = holders[i].arg;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private static class ResolvedHolder {
|
||||
boolean resolved;
|
||||
MethodParameter parameter;
|
||||
Object arg;
|
||||
|
||||
public ResolvedHolder(boolean resolved, MethodParameter parameter, Object arg) {
|
||||
this.resolved = resolved;
|
||||
this.parameter = parameter;
|
||||
this.arg = arg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler method with the given argument values.
|
||||
*/
|
||||
@Nullable
|
||||
protected Object doInvoke(Object... args) throws Exception {
|
||||
try {
|
||||
if (validator != null) {
|
||||
Method bridgedMethod = getBridgedMethod();
|
||||
Validator validator = Utils.defaultValidator();
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables()
|
||||
.validateParameters(getBean(), bridgedMethod, args);
|
||||
if (constraintViolations.size() > 0) {
|
||||
throw new ParameterValidationException(constraintViolations);
|
||||
}
|
||||
}
|
||||
return getBridgedMethod().invoke(getBean(), args);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
assertTargetBean(getBridgedMethod(), getBean(), args);
|
||||
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
|
||||
throw new IllegalStateException(formatInvokeError(text, args), ex);
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
// Unwrap for HandlerExceptionResolvers ...
|
||||
Throwable targetException = ex.getTargetException();
|
||||
if (targetException instanceof RuntimeException) {
|
||||
throw (RuntimeException) targetException;
|
||||
}
|
||||
else if (targetException instanceof Error) {
|
||||
throw (Error) targetException;
|
||||
}
|
||||
else if (targetException instanceof Exception) {
|
||||
throw (Exception) targetException;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) {
|
||||
return new AsyncResultMethodParameter(returnValue);
|
||||
}
|
||||
|
||||
private MethodParameter[] initMethodParameters() {
|
||||
int count = this.bridgedMethod.getParameterCount();
|
||||
MethodParameter[] result = new MethodParameter[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
result[i] = new HandlerMethodParameter(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bean for this handler method.
|
||||
*/
|
||||
public Object getBean() {
|
||||
return this.bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method for this handler method.
|
||||
*/
|
||||
public Method getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the type of the handler for this handler method.
|
||||
* <p>Note that if the bean type is a CGLIB-generated class, the original
|
||||
* user-defined class is returned.
|
||||
*/
|
||||
public Class<?> getBeanType() {
|
||||
return this.beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the bean method is a bridge method, this method returns the bridged
|
||||
* (user-defined) method. Otherwise it returns the same method as {@link #getMethod()}.
|
||||
*/
|
||||
protected Method getBridgedMethod() {
|
||||
return this.bridgedMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method parameters for this handler method.
|
||||
*/
|
||||
public MethodParameter[] getMethodParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HandlerMethod return type.
|
||||
*/
|
||||
public MethodParameter getReturnType() {
|
||||
return new HandlerMethodParameter(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual return value type.
|
||||
*/
|
||||
public MethodParameter getReturnValueType(@Nullable Object returnValue) {
|
||||
return new ReturnValueMethodParameter(returnValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the method return type is void, {@code false} otherwise.
|
||||
*/
|
||||
public boolean isVoid() {
|
||||
return Void.TYPE.equals(getReturnType().getParameterType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single annotation on the underlying method traversing its super methods
|
||||
* if no annotation can be found on the given method itself.
|
||||
* <p>Also supports <em>merged</em> composed annotations with attribute
|
||||
* overrides.
|
||||
* @param annotationType the type of annotation to introspect the method for
|
||||
* @return the annotation, or {@code null} if none found
|
||||
* @see AnnotatedElementUtils#findMergedAnnotation
|
||||
*/
|
||||
@Nullable
|
||||
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
|
||||
return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the parameter is declared with the given annotation type.
|
||||
* @param annotationType the annotation type to look for
|
||||
* @see AnnotatedElementUtils#hasAnnotation
|
||||
*/
|
||||
public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
|
||||
return AnnotatedElementUtils.hasAnnotation(this.method, annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HandlerMethod from which this HandlerMethod instance was
|
||||
* resolved via {@link #createWithResolvedBean()}.
|
||||
*/
|
||||
@Nullable
|
||||
public InvocableShellMethod getResolvedFromHandlerMethod() {
|
||||
return this.resolvedFromHandlerMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the provided instance contains a bean name rather than an object instance,
|
||||
* the bean name is resolved before a {@link HandlerMethod} is created and returned.
|
||||
*/
|
||||
public InvocableShellMethod createWithResolvedBean() {
|
||||
Object handler = this.bean;
|
||||
if (this.bean instanceof String) {
|
||||
Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
|
||||
String beanName = (String) this.bean;
|
||||
handler = this.beanFactory.getBean(beanName);
|
||||
}
|
||||
return new InvocableShellMethod(this, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a short representation of this handler method for log message purposes.
|
||||
*/
|
||||
public String getShortLogMessage() {
|
||||
int args = this.method.getParameterCount();
|
||||
return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof InvocableShellMethod)) {
|
||||
return false;
|
||||
}
|
||||
InvocableShellMethod otherMethod = (InvocableShellMethod) other;
|
||||
return (this.bean.equals(otherMethod.bean) && this.method.equals(otherMethod.method));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (this.bean.hashCode() * 31 + this.method.hashCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.method.toGenericString();
|
||||
}
|
||||
|
||||
|
||||
// Support methods for use in "InvocableHandlerMethod" sub-class variants..
|
||||
|
||||
@Nullable
|
||||
protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) {
|
||||
if (!ObjectUtils.isEmpty(providedArgs)) {
|
||||
for (Object providedArg : providedArgs) {
|
||||
if (parameter.getParameterType().isInstance(providedArg)) {
|
||||
return providedArg;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static String formatArgumentError(MethodParameter param, String message) {
|
||||
return "Could not resolve parameter [" + param.getParameterIndex() + "] in " +
|
||||
param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the target bean class is an instance of the class where the given
|
||||
* method is declared. In some cases the actual endpoint instance at request-
|
||||
* processing time may be a JDK dynamic proxy (lazy initialization, prototype
|
||||
* beans, and others). Endpoint classes that require proxying should prefer
|
||||
* class-based proxy mechanisms.
|
||||
*/
|
||||
protected void assertTargetBean(Method method, Object targetBean, Object[] args) {
|
||||
Class<?> methodDeclaringClass = method.getDeclaringClass();
|
||||
Class<?> targetBeanClass = targetBean.getClass();
|
||||
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
|
||||
String text = "The mapped handler method class '" + methodDeclaringClass.getName() +
|
||||
"' is not an instance of the actual endpoint bean class '" +
|
||||
targetBeanClass.getName() + "'. If the endpoint requires proxying " +
|
||||
"(e.g. due to @Transactional), please use class-based proxying.";
|
||||
throw new IllegalStateException(formatInvokeError(text, args));
|
||||
}
|
||||
}
|
||||
|
||||
protected String formatInvokeError(String text, Object[] args) {
|
||||
|
||||
String formattedArgs = IntStream.range(0, args.length)
|
||||
.mapToObj(i -> (args[i] != null ?
|
||||
"[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" :
|
||||
"[" + i + "] [null]"))
|
||||
.collect(Collectors.joining(",\n", " ", " "));
|
||||
|
||||
return text + "\n" +
|
||||
"Endpoint [" + getBeanType().getName() + "]\n" +
|
||||
"Method [" + getBridgedMethod().toGenericString() + "] " +
|
||||
"with argument values:\n" + formattedArgs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A MethodParameter with HandlerMethod-specific behavior.
|
||||
*/
|
||||
protected class HandlerMethodParameter extends SynthesizingMethodParameter {
|
||||
|
||||
public HandlerMethodParameter(int index) {
|
||||
super(InvocableShellMethod.this.bridgedMethod, index);
|
||||
}
|
||||
|
||||
protected HandlerMethodParameter(HandlerMethodParameter original) {
|
||||
super(original);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getContainingClass() {
|
||||
return InvocableShellMethod.this.getBeanType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Annotation> T getMethodAnnotation(Class<T> annotationType) {
|
||||
return InvocableShellMethod.this.getMethodAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) {
|
||||
return InvocableShellMethod.this.hasMethodAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerMethodParameter clone() {
|
||||
return new HandlerMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A MethodParameter for a HandlerMethod return type based on an actual return value.
|
||||
*/
|
||||
private class ReturnValueMethodParameter extends HandlerMethodParameter {
|
||||
|
||||
@Nullable
|
||||
private final Object returnValue;
|
||||
|
||||
public ReturnValueMethodParameter(@Nullable Object returnValue) {
|
||||
super(-1);
|
||||
this.returnValue = returnValue;
|
||||
}
|
||||
|
||||
protected ReturnValueMethodParameter(ReturnValueMethodParameter original) {
|
||||
super(original);
|
||||
this.returnValue = original.returnValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getParameterType() {
|
||||
return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReturnValueMethodParameter clone() {
|
||||
return new ReturnValueMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncResultMethodParameter extends HandlerMethodParameter {
|
||||
|
||||
@Nullable
|
||||
private final Object returnValue;
|
||||
|
||||
private final ResolvableType returnType;
|
||||
|
||||
public AsyncResultMethodParameter(@Nullable Object returnValue) {
|
||||
super(-1);
|
||||
this.returnValue = returnValue;
|
||||
this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric();
|
||||
}
|
||||
|
||||
protected AsyncResultMethodParameter(AsyncResultMethodParameter original) {
|
||||
super(original);
|
||||
this.returnValue = original.returnValue;
|
||||
this.returnType = original.returnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getParameterType() {
|
||||
if (this.returnValue != null) {
|
||||
return this.returnValue.getClass();
|
||||
}
|
||||
if (!ResolvableType.NONE.equals(this.returnType)) {
|
||||
return this.returnType.toClass();
|
||||
}
|
||||
return super.getParameterType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getGenericParameterType() {
|
||||
return this.returnType.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncResultMethodParameter clone() {
|
||||
return new AsyncResultMethodParameter(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.command.invocation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
|
||||
/**
|
||||
* Resolves method parameters by delegating to a list of registered
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
* Previously resolved method parameters are cached for faster lookups.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
public class ShellMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
|
||||
|
||||
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
|
||||
new ConcurrentHashMap<>(256);
|
||||
|
||||
|
||||
/**
|
||||
* Add the given {@link HandlerMethodArgumentResolver}.
|
||||
*/
|
||||
public ShellMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) {
|
||||
this.argumentResolvers.add(resolver);
|
||||
AnnotationAwareOrderComparator.sort(this.argumentResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
* @since 4.3
|
||||
*/
|
||||
public ShellMethodArgumentResolverComposite addResolvers(
|
||||
@Nullable HandlerMethodArgumentResolver... resolvers) {
|
||||
|
||||
if (resolvers != null) {
|
||||
Collections.addAll(this.argumentResolvers, resolvers);
|
||||
}
|
||||
AnnotationAwareOrderComparator.sort(this.argumentResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
|
||||
*/
|
||||
public ShellMethodArgumentResolverComposite addResolvers(
|
||||
@Nullable List<? extends HandlerMethodArgumentResolver> resolvers) {
|
||||
|
||||
if (resolvers != null) {
|
||||
this.argumentResolvers.addAll(resolvers);
|
||||
}
|
||||
AnnotationAwareOrderComparator.sort(this.argumentResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a read-only list with the contained resolvers, or an empty list.
|
||||
*/
|
||||
public List<HandlerMethodArgumentResolver> getResolvers() {
|
||||
return Collections.unmodifiableList(this.argumentResolvers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the list of configured resolvers and the resolver cache.
|
||||
*/
|
||||
public void clear() {
|
||||
this.argumentResolvers.clear();
|
||||
this.argumentResolverCache.clear();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether the given {@linkplain MethodParameter method parameter} is
|
||||
* supported by any registered {@link HandlerMethodArgumentResolver}.
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return getArgumentResolver(parameter) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over registered
|
||||
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}
|
||||
* and invoke the one that supports it.
|
||||
* @throws IllegalArgumentException if no suitable argument resolver is found
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
|
||||
if (resolver == null) {
|
||||
throw new IllegalArgumentException("Unsupported parameter type [" +
|
||||
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
|
||||
}
|
||||
return resolver.resolveArgument(parameter, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a registered {@link HandlerMethodArgumentResolver} that supports
|
||||
* the given method parameter.
|
||||
*/
|
||||
@Nullable
|
||||
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
|
||||
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
|
||||
if (result == null) {
|
||||
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
|
||||
if (resolver.supportsParameter(parameter)) {
|
||||
result = resolver;
|
||||
this.argumentResolverCache.put(parameter, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.completion;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
/**
|
||||
* Interface resolving completion proposals.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface CompletionResolver {
|
||||
|
||||
/**
|
||||
* Resolve completions.
|
||||
*
|
||||
* @param registration the command registration
|
||||
* @param context the completion context
|
||||
* @return list of resolved completions
|
||||
*/
|
||||
List<CompletionProposal> resolve(CommandRegistration registration, CompletionContext context);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.completion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link CompletionResolver}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class DefaultCompletionResolver implements CompletionResolver {
|
||||
|
||||
@Override
|
||||
public List<CompletionProposal> resolve(CommandRegistration registration, CompletionContext context) {
|
||||
List<CompletionProposal> candidates = new ArrayList<>();
|
||||
registration.getOptions().stream()
|
||||
.flatMap(o -> Stream.of(o.getLongNames()))
|
||||
.map(ln -> new CompletionProposal("--" + ln))
|
||||
.forEach(candidates::add);
|
||||
registration.getOptions().stream()
|
||||
.flatMap(o -> Stream.of(o.getShortNames()))
|
||||
.map(ln -> new CompletionProposal("-" + ln))
|
||||
.forEach(candidates::add);
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;;
|
||||
|
||||
/**
|
||||
* Displays command parsing errors on the terminal.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class CommandParserExceptionsExceptionResultHandler extends TerminalAwareResultHandler<CommandParserExceptionsException> {
|
||||
|
||||
public CommandParserExceptionsExceptionResultHandler(Terminal terminal) {
|
||||
super(terminal);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doHandleResult(CommandParserExceptionsException result) {
|
||||
AttributedStringBuilder builder = new AttributedStringBuilder();
|
||||
result.getParserExceptions().stream().forEach(e -> {
|
||||
builder.append(new AttributedString(e.getMessage(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)));
|
||||
builder.append("\n");
|
||||
});
|
||||
terminal.writer().append(builder.toAnsi());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 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,33 +13,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import javax.validation.ElementKind;
|
||||
import javax.validation.Path;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.ParameterValidationException;
|
||||
import org.springframework.shell.Utils;
|
||||
|
||||
/**
|
||||
* Displays validation errors on the terminal.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ParameterValidationExceptionResultHandler
|
||||
extends TerminalAwareResultHandler<ParameterValidationException> {
|
||||
@@ -48,48 +34,15 @@ public class ParameterValidationExceptionResultHandler
|
||||
super(terminal);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private List<ParameterResolver> parameterResolvers;
|
||||
|
||||
@Override
|
||||
protected void doHandleResult(ParameterValidationException result) {
|
||||
terminal.writer().println(new AttributedString("The following constraints were not met:",
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
result.getConstraintViolations().stream()
|
||||
.forEach(v -> {
|
||||
Optional<Integer> parameterIndex = StreamSupport.stream(v.getPropertyPath().spliterator(), false)
|
||||
.filter(n -> n.getKind() == ElementKind.PARAMETER)
|
||||
.map(n -> ((Path.ParameterNode) n).getParameterIndex())
|
||||
.findFirst();
|
||||
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(result.getMethodTarget().getMethod(),
|
||||
parameterIndex.get());
|
||||
List<ParameterDescription> descriptions = findParameterResolver(methodParameter)
|
||||
.describe(methodParameter).collect(Collectors.toList());
|
||||
if (descriptions.size() == 1) {
|
||||
ParameterDescription description = descriptions.get(0);
|
||||
AttributedStringBuilder ansi = new AttributedStringBuilder(100);
|
||||
ansi.append("\t").append(description.keys().get(0), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).bold());
|
||||
ansi.append(" ").append(description.formal(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).underline());
|
||||
String msg = String.format(" : %s (You passed '%s')",
|
||||
v.getMessage(),
|
||||
String.valueOf(v.getInvalidValue())
|
||||
);
|
||||
ansi.append(msg, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
|
||||
|
||||
terminal.writer().println(ansi.toAnsi(terminal));
|
||||
}
|
||||
// Several formals for one method param, must be framework like JCommander, etc
|
||||
else {
|
||||
// Output toString() for now...
|
||||
terminal.writer().println(new AttributedString(v.toString(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi(terminal));
|
||||
}
|
||||
|
||||
terminal.writer().println(new AttributedString(v.toString(),
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi(terminal));
|
||||
});
|
||||
}
|
||||
|
||||
private ParameterResolver findParameterResolver(MethodParameter methodParameter) {
|
||||
return parameterResolvers.stream().filter(pr -> pr.supports(methodParameter)).findFirst().get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.result;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
@@ -22,8 +21,8 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.TerminalSizeAware;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.jline.InteractiveShellRunner;
|
||||
|
||||
/**
|
||||
@@ -57,9 +56,13 @@ public class ResultHandlerConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThrowableResultHandler throwableResultHandler(Terminal terminal, CommandRegistry commandRegistry,
|
||||
ObjectProvider<InteractiveShellRunner> interactiveApplicationRunner) {
|
||||
return new ThrowableResultHandler(terminal, commandRegistry, interactiveApplicationRunner);
|
||||
public CommandParserExceptionsExceptionResultHandler commandParserExceptionsExceptionResultHandler(Terminal terminal) {
|
||||
return new CommandParserExceptionsExceptionResultHandler(terminal);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThrowableResultHandler throwableResultHandler(Terminal terminal, CommandCatalog commandCatalog,
|
||||
ObjectProvider<InteractiveShellRunner> interactiveApplicationRunner) {
|
||||
return new ThrowableResultHandler(terminal, commandCatalog, interactiveApplicationRunner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Copyright 2015-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.
|
||||
@@ -22,8 +22,8 @@ import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.jline.InteractiveShellRunner;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -43,14 +43,14 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable
|
||||
|
||||
private Throwable lastError;
|
||||
|
||||
private CommandRegistry commandRegistry;
|
||||
private CommandCatalog commandCatalog;
|
||||
|
||||
private ObjectProvider<InteractiveShellRunner> interactiveRunner;
|
||||
|
||||
public ThrowableResultHandler(Terminal terminal, CommandRegistry commandRegistry,
|
||||
public ThrowableResultHandler(Terminal terminal, CommandCatalog commandCatalog,
|
||||
ObjectProvider<InteractiveShellRunner> interactiveRunner) {
|
||||
super(terminal);
|
||||
this.commandRegistry = commandRegistry;
|
||||
this.commandCatalog = commandCatalog;
|
||||
this.interactiveRunner = interactiveRunner;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler<Throwable
|
||||
String toPrint = StringUtils.hasLength(result.getMessage()) ? result.getMessage() : result.toString();
|
||||
terminal.writer().println(new AttributedString(toPrint,
|
||||
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi());
|
||||
if (interactiveRunner.getIfAvailable() != null && commandRegistry.listCommands().containsKey(DETAILS_COMMAND_NAME)) {
|
||||
if (interactiveRunner.getIfAvailable() != null && commandCatalog.getRegistrations().keySet().contains(DETAILS_COMMAND_NAME)) {
|
||||
terminal.writer().println(
|
||||
new AttributedStringBuilder()
|
||||
.append("Details of the error have been omitted. You can use the ", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED))
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanExpressionContext;
|
||||
import org.springframework.beans.factory.config.BeanExpressionResolver;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.annotation.ValueConstants;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class to resolve method arguments from a named value, e.g.
|
||||
* message headers or destination variables. Named values could have one or more
|
||||
* of a name, a required flag, and a default value.
|
||||
*
|
||||
* <p>Subclasses only need to define specific steps such as how to obtain named
|
||||
* value details from a method parameter, how to resolve to argument values, or
|
||||
* how to handle missing values.
|
||||
*
|
||||
* <p>A default value string can contain ${...} placeholders and Spring
|
||||
* Expression Language {@code #{...}} expressions which will be resolved if a
|
||||
* {@link ConfigurableBeanFactory} is supplied to the class constructor.
|
||||
*
|
||||
* <p>A {@link ConversionService} is used to convert a resolved String argument
|
||||
* value to the expected target method parameter type.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
public static final String ARGUMENT_PREFIX = "springShellArgument.";
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
@Nullable
|
||||
private final ConfigurableBeanFactory configurableBeanFactory;
|
||||
|
||||
@Nullable
|
||||
private final BeanExpressionContext expressionContext;
|
||||
|
||||
private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
|
||||
|
||||
/**
|
||||
* Constructor with a {@link ConversionService} and a {@link BeanFactory}.
|
||||
* @param conversionService conversion service for converting String values
|
||||
* to the target method parameter type
|
||||
* @param beanFactory a bean factory for resolving {@code ${...}}
|
||||
* placeholders and {@code #{...}} SpEL expressions in default values
|
||||
*/
|
||||
protected AbstractArgumentMethodArgumentResolver(ConversionService conversionService,
|
||||
@Nullable ConfigurableBeanFactory beanFactory) {
|
||||
|
||||
// Fallback on shared ConversionService for now for historic reasons.
|
||||
// Possibly remove after discussion in gh-23882.
|
||||
|
||||
//noinspection ConstantConditions
|
||||
this.conversionService = (conversionService != null ?
|
||||
conversionService : DefaultConversionService.getSharedInstance());
|
||||
|
||||
this.configurableBeanFactory = beanFactory;
|
||||
this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
|
||||
MethodParameter nestedParameter = parameter.nestedIfOptional();
|
||||
|
||||
Object arg = resolveArgumentInternal(nestedParameter, message, namedValueInfo.names);
|
||||
if (arg == null) {
|
||||
if (namedValueInfo.defaultValue != null) {
|
||||
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
|
||||
}
|
||||
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
|
||||
handleMissingValue(namedValueInfo.names, nestedParameter, message);
|
||||
}
|
||||
arg = handleNullValue(namedValueInfo.names, arg, nestedParameter.getNestedParameterType());
|
||||
}
|
||||
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
|
||||
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
|
||||
}
|
||||
|
||||
if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) {
|
||||
arg = this.conversionService.convert(arg, TypeDescriptor.forObject(arg), new TypeDescriptor(parameter));
|
||||
// Check for null value after conversion of incoming argument value
|
||||
if (arg == null && namedValueInfo.defaultValue == null &&
|
||||
namedValueInfo.required && !nestedParameter.isOptional()) {
|
||||
handleMissingValue(namedValueInfo.names, nestedParameter, message);
|
||||
}
|
||||
}
|
||||
|
||||
handleResolvedValue(arg, namedValueInfo.names, parameter, message);
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the named value for the given method parameter.
|
||||
*/
|
||||
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
|
||||
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
|
||||
if (namedValueInfo == null) {
|
||||
namedValueInfo = createNamedValueInfo(parameter);
|
||||
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
|
||||
this.namedValueInfoCache.put(parameter, namedValueInfo);
|
||||
}
|
||||
return namedValueInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the {@link NamedValueInfo} object for the given method parameter.
|
||||
* Implementations typically retrieve the method annotation by means of
|
||||
* {@link MethodParameter#getParameterAnnotation(Class)}.
|
||||
* @param parameter the method parameter
|
||||
* @return the named value information
|
||||
*/
|
||||
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Fall back on the parameter name from the class file if necessary and
|
||||
* replace {@link ValueConstants#DEFAULT_NONE} with null.
|
||||
*/
|
||||
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
|
||||
List<String> names = info.names;
|
||||
if (info.names.isEmpty()) {
|
||||
String name = parameter.getParameterName();
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Name for argument of type [" + parameter.getNestedParameterType().getName() +
|
||||
"] not specified, and parameter name information not found in class file either.");
|
||||
}
|
||||
names.add(name);
|
||||
}
|
||||
return new NamedValueInfo(names, info.required,
|
||||
ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the given annotation-specified value,
|
||||
* potentially containing placeholders and expressions.
|
||||
*/
|
||||
@Nullable
|
||||
private Object resolveEmbeddedValuesAndExpressions(String value) {
|
||||
if (this.configurableBeanFactory == null || this.expressionContext == null) {
|
||||
return value;
|
||||
}
|
||||
String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
|
||||
BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
|
||||
if (exprResolver == null) {
|
||||
return value;
|
||||
}
|
||||
return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the given parameter type and value name into an argument value.
|
||||
* @param parameter the method parameter to resolve to an argument value
|
||||
* @param message the current request
|
||||
* @param name the name of the value being resolved
|
||||
* @return the resolved argument. May be {@code null}
|
||||
* @throws Exception in case of errors
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, List<String> names)
|
||||
throws Exception;
|
||||
|
||||
/**
|
||||
* Invoked when a value is required, but {@link #resolveArgumentInternal}
|
||||
* returned {@code null} and there is no default value. Sub-classes can
|
||||
* throw an appropriate exception for this case.
|
||||
* @param names the name for the value
|
||||
* @param parameter the target method parameter
|
||||
* @param message the message being processed
|
||||
*/
|
||||
protected abstract void handleMissingValue(List<String> names, MethodParameter parameter, Message<?> message);
|
||||
|
||||
/**
|
||||
* One last chance to handle a possible null value.
|
||||
* Specifically for booleans method parameters, use {@link Boolean#FALSE}.
|
||||
* Also raise an ISE for primitive types.
|
||||
*/
|
||||
@Nullable
|
||||
private Object handleNullValue(List<String> name, @Nullable Object value, Class<?> paramType) {
|
||||
if (value == null) {
|
||||
if (Boolean.TYPE.equals(paramType)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
else if (paramType.isPrimitive()) {
|
||||
throw new IllegalStateException("Optional " + paramType + " parameter '" + name +
|
||||
"' is present but cannot be translated into a null value due to being " +
|
||||
"declared as a primitive type. Consider declaring it as object wrapper " +
|
||||
"for the corresponding primitive type.");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a value is resolved.
|
||||
* @param arg the resolved argument value
|
||||
* @param name the argument name
|
||||
* @param parameter the argument parameter type
|
||||
* @param message the message
|
||||
*/
|
||||
protected void handleResolvedValue(
|
||||
@Nullable Object arg, List<String> name, MethodParameter parameter, Message<?> message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a named value declaration.
|
||||
*/
|
||||
protected static class NamedValueInfo {
|
||||
|
||||
private final List<String> names;
|
||||
|
||||
private final boolean required;
|
||||
|
||||
@Nullable
|
||||
private final String defaultValue;
|
||||
|
||||
protected NamedValueInfo(List<String> names, boolean required, @Nullable String defaultValue) {
|
||||
this.names = names;
|
||||
this.required = required;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ConfigurableCommandRegistry}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class ConfigurableCommandRegistryTest {
|
||||
|
||||
@Test
|
||||
public void testRegistration() {
|
||||
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
registry.register("foo", MethodTarget.of("toString", this, new Command.Help("some command")));
|
||||
assertThat(registry.listCommands()).containsKeys("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleRegistration() {
|
||||
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
registry.register("foo", MethodTarget.of("toString", this, new Command.Help("some command")));
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registry.register("foo", MethodTarget.of("hashCode", this, new Command.Help("some command")));
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("foo")
|
||||
.hasMessageContaining("toString")
|
||||
.hasMessageContaining("hashCode");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 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,12 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -31,6 +29,10 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -44,7 +46,7 @@ import static org.mockito.Mockito.when;
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ShellTest {
|
||||
public class ShellTests {
|
||||
|
||||
@Mock
|
||||
private InputProvider inputProvider;
|
||||
@@ -53,12 +55,10 @@ public class ShellTest {
|
||||
ResultHandlerService resultHandlerService;
|
||||
|
||||
@Mock
|
||||
CommandRegistry commandRegistry;
|
||||
CommandCatalog commandRegistry;
|
||||
|
||||
@Mock
|
||||
private ParameterResolver parameterResolver;
|
||||
|
||||
private ValueResult valueResult;
|
||||
private CompletionResolver completionResolver;
|
||||
|
||||
@InjectMocks
|
||||
private Shell shell;
|
||||
@@ -67,26 +67,30 @@ public class ShellTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
shell.parameterResolvers = Arrays.asList(parameterResolver);
|
||||
shell.setCompletionResolvers(Arrays.asList(completionResolver));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandMatch() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
|
||||
valueResult = new ValueResult(null, "test");
|
||||
when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(any());
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(Collections.singletonMap("hello world",
|
||||
MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
fail("Exit expected");
|
||||
}
|
||||
catch (Exit expected) {
|
||||
|
||||
System.out.println(expected);
|
||||
}
|
||||
|
||||
assertThat(invoked).isTrue();
|
||||
@@ -97,8 +101,15 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(isA(CommandNotFound.class));
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(Collections.singletonMap("bonjour",
|
||||
MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("bonjour")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -115,8 +126,15 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "helloworld how are you doing ?");
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(isA(CommandNotFound.class));
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(
|
||||
Collections.singletonMap("hello", MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -129,14 +147,18 @@ public class ShellTest {
|
||||
|
||||
@Test
|
||||
public void noCommand() throws IOException {
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(inputProvider.readInput()).thenReturn(() -> "", () -> "hello world how are you doing ?", null);
|
||||
valueResult = new ValueResult(null, "test");
|
||||
when(parameterResolver.resolve(any(), any())).thenReturn(valueResult);
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(any());
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(Collections.singletonMap("hello world",
|
||||
MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -154,8 +176,16 @@ public class ShellTest {
|
||||
when(inputProvider.readInput()).thenReturn(() -> "fail");
|
||||
doThrow(new Exit()).when(resultHandlerService).handle(isA(SomeException.class));
|
||||
|
||||
when(commandRegistry.listCommands()).thenReturn(Collections.singletonMap("fail",
|
||||
MethodTarget.of("failing", this, new Command.Help("Will throw an exception"))));
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("fail")
|
||||
.withTarget()
|
||||
.method(this, "failing")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("fail", registration);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
|
||||
try {
|
||||
shell.run(inputProvider);
|
||||
@@ -177,17 +207,27 @@ public class ShellTest {
|
||||
|
||||
@Test
|
||||
public void commandNameCompletion() throws Exception {
|
||||
Map<String, MethodTarget> methodTargets = new HashMap<>();
|
||||
methodTargets.put("hello world", MethodTarget.of("helloWorld", this, new Command.Help("hellow world")));
|
||||
methodTargets.put("another command", MethodTarget.of("helloWorld", this, new Command.Help("another command")));
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
when(commandRegistry.listCommands()).thenReturn(methodTargets);
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
CommandRegistration registration2 = CommandRegistration.builder()
|
||||
.command("another command")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration1);
|
||||
registrations.put("another command", registration2);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
// Invoke at very start
|
||||
List<String> proposals = shell.complete(new CompletionContext(Arrays.asList(""), 0, "".length()))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(proposals).containsExactlyInAnyOrder("another command", "hello world");
|
||||
// assertThat(proposals).containsExactly("another command", "hello world");
|
||||
|
||||
// Invoke in middle of first word
|
||||
proposals = shell.complete(new CompletionContext(Arrays.asList("hel"), 0, "hel".length()))
|
||||
@@ -231,6 +271,51 @@ public class ShellTest {
|
||||
throw new SomeException();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completionArgWithMethod() throws Exception {
|
||||
when(completionResolver.resolve(any(), any())).thenReturn(Arrays.asList(new CompletionProposal("--arg1")));
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.method(this, "helloWorld")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("arg1 desc")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration1);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
List<String> proposals = shell.complete(new CompletionContext(Arrays.asList("hello", "world", ""), 2, "".length()))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(proposals).containsExactlyInAnyOrder("--arg1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completionArgWithFunction() throws Exception {
|
||||
when(completionResolver.resolve(any(), any())).thenReturn(Arrays.asList(new CompletionProposal("--arg1")));
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("hello world")
|
||||
.withTarget()
|
||||
.function(ctx -> {
|
||||
return null;
|
||||
})
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("arg1 desc")
|
||||
.and()
|
||||
.build();
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("hello world", registration1);
|
||||
when(commandRegistry.getRegistrations()).thenReturn(registrations);
|
||||
|
||||
List<String> proposals = shell.complete(new CompletionContext(Arrays.asList("hello", "world", ""), 2, "".length()))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(proposals).containsExactlyInAnyOrder("--arg1");
|
||||
}
|
||||
|
||||
private static class Exit extends RuntimeException {
|
||||
}
|
||||
@@ -238,6 +323,4 @@ public class ShellTest {
|
||||
private static class SomeException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Utils}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class UtilsTest {
|
||||
|
||||
@Test
|
||||
public void testUnCamelify() throws Exception {
|
||||
assertThat(Utils.unCamelify("HelloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorldHowAreYou")).isEqualTo("hello-world-how-are-you");
|
||||
assertThat(Utils.unCamelify("URL")).isEqualTo("url");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2015-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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Utils}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class UtilsTests {
|
||||
|
||||
@Test
|
||||
public void testUnCamelify() throws Exception {
|
||||
assertThat(Utils.unCamelify("HelloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorld")).isEqualTo("hello-world");
|
||||
assertThat(Utils.unCamelify("helloWorldHowAreYou")).isEqualTo("hello-world-how-are-you");
|
||||
assertThat(Utils.unCamelify("URL")).isEqualTo("url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSplit() {
|
||||
Predicate<String> predicate = t -> t.startsWith("-");
|
||||
List<List<String>> split = null;
|
||||
|
||||
split = Utils.split(new String[] { "-a1", "a1" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("-a1", "a1"));
|
||||
|
||||
split = Utils.split(new String[] { "-a1", "a1", "-a2", "a2" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("-a1", "a1"), Arrays.asList("-a2", "a2"));
|
||||
|
||||
split = Utils.split(new String[] { "a0", "-a1", "a1" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("a0"), Arrays.asList("-a1", "a1"));
|
||||
|
||||
split = Utils.split(new String[] { "-a1", "-a2" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("-a1"), Arrays.asList("-a2"));
|
||||
|
||||
split = Utils.split(new String[] { "a1", "a2" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("a1", "a2"));
|
||||
|
||||
split = Utils.split(new String[] { "-a1", "a1", "a2" }, predicate);
|
||||
assertThat(split).containsExactly(Arrays.asList("-a1", "a1", "a2"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
|
||||
public abstract class AbstractCommandTests {
|
||||
|
||||
protected Pojo1 pojo1;
|
||||
|
||||
protected Function<CommandContext, String> function1 = ctx -> {
|
||||
String arg1 = ctx.getOptionValue("arg1");
|
||||
return "hi" + arg1;
|
||||
};
|
||||
|
||||
protected Function<CommandContext, Void> function2 = ctx -> {
|
||||
return null;
|
||||
};
|
||||
|
||||
@BeforeEach
|
||||
public void setupAbstractCommandTests() {
|
||||
pojo1 = new Pojo1();
|
||||
}
|
||||
|
||||
protected static class Pojo1 {
|
||||
|
||||
public int method1Count;
|
||||
public CommandContext method1Ctx;
|
||||
public int method2Count;
|
||||
public int method3Count;
|
||||
public int method4Count;
|
||||
public String method4Arg1;
|
||||
public Boolean method5ArgA;
|
||||
public Boolean method5ArgB;
|
||||
public Boolean method5ArgC;
|
||||
public int method6Count;
|
||||
public String method6Arg1;
|
||||
public String method6Arg2;
|
||||
public String method6Arg3;
|
||||
public int method7Count;
|
||||
public int method7Arg1;
|
||||
public int method7Arg2;
|
||||
public int method7Arg3;
|
||||
public int method8Count;
|
||||
public float[] method8Arg1;
|
||||
|
||||
public void method1(CommandContext ctx) {
|
||||
method1Ctx = ctx;
|
||||
method1Count++;
|
||||
}
|
||||
|
||||
public String method2() {
|
||||
method2Count++;
|
||||
return "hi";
|
||||
}
|
||||
|
||||
public String method3(@Header("arg1") String arg1) {
|
||||
method3Count++;
|
||||
return "hi" + arg1;
|
||||
}
|
||||
|
||||
public String method4(String arg1) {
|
||||
method4Arg1 = arg1;
|
||||
method4Count++;
|
||||
return "hi" + arg1;
|
||||
}
|
||||
|
||||
public void method5(@Header("a") boolean a, @Header("b") boolean b, @Header("c") boolean c) {
|
||||
method5ArgA = a;
|
||||
method5ArgB = b;
|
||||
method5ArgC = c;
|
||||
}
|
||||
|
||||
public String method6(String arg1, String arg2, String arg3) {
|
||||
method6Arg1 = arg1;
|
||||
method6Arg2 = arg2;
|
||||
method6Arg3 = arg3;
|
||||
method6Count++;
|
||||
return "hi" + arg1 + arg2 + arg3;
|
||||
}
|
||||
|
||||
public void method7(int arg1, int arg2, int arg3) {
|
||||
method7Arg1 = arg1;
|
||||
method7Arg2 = arg2;
|
||||
method7Arg3 = arg3;
|
||||
method7Count++;
|
||||
}
|
||||
|
||||
public void method8(float[] arg1) {
|
||||
method8Arg1 = arg1;
|
||||
method8Count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class CommandCatalogTests extends AbstractCommandTests {
|
||||
|
||||
@Test
|
||||
public void testCommandCatalog () {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("group1 sub1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
CommandCatalog catalog = CommandCatalog.of();
|
||||
catalog.register(r1);
|
||||
assertThat(catalog.getRegistrations()).hasSize(1);
|
||||
catalog.unregister(r1);
|
||||
assertThat(catalog.getRegistrations()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolver() {
|
||||
// catalog itself would not have any registered command but
|
||||
// this custom resolver adds one which may dymanically go away.
|
||||
DynamicCommandResolver resolver = new DynamicCommandResolver();
|
||||
CommandCatalog catalog = CommandCatalog.of(Arrays.asList(resolver), null);
|
||||
assertThat(catalog.getRegistrations()).hasSize(1);
|
||||
resolver.enabled = false;
|
||||
assertThat(catalog.getRegistrations()).hasSize(0);
|
||||
}
|
||||
|
||||
class DynamicCommandResolver implements CommandResolver {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("group1 sub1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
boolean enabled = true;
|
||||
|
||||
@Override
|
||||
public List<CommandRegistration> resolve() {
|
||||
List<CommandRegistration> regs = new ArrayList<>();
|
||||
if (enabled) {
|
||||
regs.add(r1);
|
||||
}
|
||||
return regs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionArity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class CommandExecutionTests extends AbstractCommandTests {
|
||||
|
||||
private CommandExecution execution;
|
||||
|
||||
@BeforeEach
|
||||
public void setupCommandExecutionTests() {
|
||||
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
|
||||
resolvers.add(new ArgumentHeaderMethodArgumentResolver(new DefaultConversionService(), null));
|
||||
resolvers.add(new CommandContextMethodArgumentResolver());
|
||||
execution = CommandExecution.of(resolvers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFunctionExecution() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodExecution1() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
assertThat(pojo1.method3Count).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodExecution2() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method1")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
assertThat(pojo1.method1Count).isEqualTo(1);
|
||||
assertThat(pojo1.method1Ctx).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSinglePositionalArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value"});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodSingleWithNamedArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value");
|
||||
assertThat(result).isEqualTo("himyarg1value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMultiPositionalArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMultiPositionalArgsAll() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.ONE_OR_MORE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value1 myarg1value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMultipleArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.description("some arg2")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg3")
|
||||
.description("some arg3")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method6")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"--arg1", "myarg1value", "--arg2", "myarg2value", "--arg3", "myarg3value"});
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
assertThat(pojo1.method6Arg3).isEqualTo("myarg3value");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMethodMultipleIntArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.description("some arg2")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg3")
|
||||
.description("some arg3")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method7")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"--arg1", "1", "--arg2", "2", "--arg3", "3"});
|
||||
assertThat(pojo1.method7Count).isEqualTo(1);
|
||||
assertThat(pojo1.method7Arg1).isEqualTo(1);
|
||||
assertThat(pojo1.method7Arg2).isEqualTo(2);
|
||||
assertThat(pojo1.method7Arg3).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodMultiplePositionalStringArgs() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.description("some arg2")
|
||||
.position(1)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg3")
|
||||
.description("some arg3")
|
||||
.position(2)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method6")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
execution.evaluate(r1, new String[]{"myarg1value", "myarg2value", "myarg3value"});
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
assertThat(pojo1.method6Arg3).isEqualTo("myarg3value");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"myarg1value --arg2 myarg2value --arg3 myarg3value",
|
||||
"--arg1 myarg1value myarg2value --arg3 myarg3value",
|
||||
"--arg1 myarg1value --arg2 myarg2value myarg3value"
|
||||
})
|
||||
public void testMethodMultiplePositionalStringArgsMixed(String arg) {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.position(0)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.description("some arg2")
|
||||
.position(1)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg3")
|
||||
.description("some arg3")
|
||||
.position(2)
|
||||
.arity(OptionArity.EXACTLY_ONE)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method6")
|
||||
.and()
|
||||
.build();
|
||||
String[] args = arg.split(" ");
|
||||
// execution.evaluate(r1, new String[]{"myarg1value", "--arg2", "myarg2value", "--arg3", "myarg3value"});
|
||||
execution.evaluate(r1, args);
|
||||
assertThat(pojo1.method6Count).isEqualTo(1);
|
||||
assertThat(pojo1.method6Arg1).isEqualTo("myarg1value");
|
||||
assertThat(pojo1.method6Arg2).isEqualTo("myarg2value");
|
||||
assertThat(pojo1.method6Arg3).isEqualTo("myarg3value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortCombinedWithoutValue() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('a')
|
||||
.description("short arg a")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('b')
|
||||
.description("short arg b")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('c')
|
||||
.description("short arg c")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method5")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"-abc"});
|
||||
assertThat(pojo1.method5ArgA).isTrue();
|
||||
assertThat(pojo1.method5ArgB).isTrue();
|
||||
assertThat(pojo1.method5ArgC).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortCombinedSomeHavingValue() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('a')
|
||||
.description("short arg a")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('b')
|
||||
.description("short arg b")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withOption()
|
||||
.shortNames('c')
|
||||
.description("short arg c")
|
||||
.type(boolean.class)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method5")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"-ac", "-b", "false"});
|
||||
assertThat(pojo1.method5ArgA).isTrue();
|
||||
assertThat(pojo1.method5ArgB).isFalse();
|
||||
assertThat(pojo1.method5ArgC).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFloatArrayOne() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.type(float[].class)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method8")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "0.1"});
|
||||
assertThat(pojo1.method8Count).isEqualTo(1);
|
||||
assertThat(pojo1.method8Arg1).isEqualTo(new float[]{0.1f});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFloatArrayTwo() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.type(float[].class)
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method8")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{"--arg1", "0.1", "0.2"});
|
||||
assertThat(pojo1.method8Count).isEqualTo(1);
|
||||
assertThat(pojo1.method8Arg1).isEqualTo(new float[]{0.1f, 0.2f});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultValueAsNull() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
execution.evaluate(r1, new String[]{});
|
||||
assertThat(pojo1.method4Count).isEqualTo(1);
|
||||
assertThat(pojo1.method4Arg1).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredArg() {
|
||||
CommandRegistration r1 = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method4")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
execution.evaluate(r1, new String[]{});
|
||||
}).isInstanceOf(CommandParserExceptionsException.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.shell.command.CommandParser.CommandParserResults;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class CommandParserTests extends AbstractCommandTests {
|
||||
|
||||
private CommandParser parser;
|
||||
|
||||
@BeforeEach
|
||||
public void setupCommandParserTests() {
|
||||
parser = CommandParser.of();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyOptionsAndArgs() {
|
||||
CommandParserResults results = parser.parse(Collections.emptyList(), new String[0]);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongName() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortName() {
|
||||
CommandOption option1 = shortOption('a');
|
||||
CommandOption option2 = shortOption('b');
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"-a", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleArgs() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo", "--arg2", "bar"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo");
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleArgsWithMultiValues() {
|
||||
CommandOption option1 = longOption("arg1", null, false, null, 1, 2);
|
||||
CommandOption option2 = longOption("arg2", null, false, null, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo1", "foo2", "--arg2", "bar1", "bar2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo1 foo2");
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo("bar1 bar2");
|
||||
assertThat(results.positional()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBooleanWithoutArg() {
|
||||
ResolvableType type = ResolvableType.forType(boolean.class);
|
||||
CommandOption option1 = shortOption('v', type);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"-v"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBooleanWithArg() {
|
||||
ResolvableType type = ResolvableType.forType(boolean.class);
|
||||
CommandOption option1 = shortOption('v', type);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"-v", "false"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingRequiredOption() {
|
||||
CommandOption option1 = longOption("arg1", true);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.errors()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpaceInArgWithOneArg() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "foo bar"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpaceInArgWithMultipleArgs() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
CommandOption option2 = longOption("arg2");
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "foo bar", "--arg2", "hi"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("foo bar");
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(1).value()).isEqualTo("hi");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgs() {
|
||||
String[] args = new String[]{"arg1", "arg2"};
|
||||
CommandParserResults results = parser.parse(Collections.emptyList(), args);
|
||||
assertThat(results.results()).hasSize(0);
|
||||
assertThat(results.positional()).containsExactly("arg1", "arg2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgBeforeOption() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"foo", "--arg1", "value"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value");
|
||||
assertThat(results.positional()).containsExactly("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgAfterOption() {
|
||||
CommandOption option1 = longOption("arg1");
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value");
|
||||
assertThat(results.positional()).containsExactly("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgWithoutOption() {
|
||||
CommandOption option1 = longOption("arg1", 0, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value foo");
|
||||
assertThat(results.positional()).containsExactly("value", "foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonMappedArgWithoutOptionHavingType() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(String.class), false, 0, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"value", "foo"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("value foo");
|
||||
assertThat(results.positional()).containsExactly("value", "foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombined() {
|
||||
CommandOption optionA = shortOption('a');
|
||||
CommandOption optionB = shortOption('b');
|
||||
CommandOption optionC = shortOption('c');
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isNull();
|
||||
assertThat(results.results().get(1).value()).isNull();
|
||||
assertThat(results.results().get(2).value()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanType() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanTypeArgFalse() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-abc", "false"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(false);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(false);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortOptionsCombinedBooleanTypeSomeArgFalse() {
|
||||
CommandOption optionA = shortOption('a', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionB = shortOption('b', ResolvableType.forType(boolean.class));
|
||||
CommandOption optionC = shortOption('c', ResolvableType.forType(boolean.class));
|
||||
List<CommandOption> options = Arrays.asList(optionA, optionB, optionC);
|
||||
String[] args = new String[]{"-ac", "-b", "false"};
|
||||
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(3);
|
||||
assertThat(results.results().get(0).option()).isSameAs(optionA);
|
||||
assertThat(results.results().get(1).option()).isSameAs(optionC);
|
||||
assertThat(results.results().get(2).option()).isSameAs(optionB);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(1).value()).isEqualTo(true);
|
||||
assertThat(results.results().get(2).value()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongOptionsWithArray() {
|
||||
CommandOption option1 = longOption("arg1", ResolvableType.forType(int[].class));
|
||||
List<CommandOption> options = Arrays.asList(option1);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(1);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(0).value()).isEqualTo(new String[] { "1", "2" });
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapPositionalArgs1() {
|
||||
CommandOption option1 = longOption("arg1", 0, 1, 1);
|
||||
CommandOption option2 = longOption("arg2", 1, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"--arg1", "1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("1");
|
||||
assertThat(results.results().get(1).value()).isEqualTo("2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapPositionalArgs2() {
|
||||
CommandOption option1 = longOption("arg1", 0, 1, 1);
|
||||
CommandOption option2 = longOption("arg2", 1, 1, 2);
|
||||
List<CommandOption> options = Arrays.asList(option1, option2);
|
||||
String[] args = new String[]{"1", "2"};
|
||||
CommandParserResults results = parser.parse(options, args);
|
||||
assertThat(results.results()).hasSize(2);
|
||||
assertThat(results.results().get(0).option()).isSameAs(option1);
|
||||
assertThat(results.results().get(1).option()).isSameAs(option2);
|
||||
assertThat(results.results().get(0).value()).isEqualTo("1");
|
||||
assertThat(results.results().get(1).value()).isEqualTo("2");
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name) {
|
||||
return longOption(name, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, boolean required) {
|
||||
return longOption(name, null, required, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type) {
|
||||
return longOption(name, type, false, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, int position, int arityMin, int arityMax) {
|
||||
return longOption(name, null, false, position, arityMin, arityMax);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type, boolean required, Integer position) {
|
||||
return longOption(name, type, required, position, null, null);
|
||||
}
|
||||
|
||||
private static CommandOption longOption(String name, ResolvableType type, boolean required, Integer position, Integer arityMin, Integer arityMax) {
|
||||
return CommandOption.of(new String[] { name }, new Character[0], "desc", type, required, null, position,
|
||||
arityMin, arityMax);
|
||||
}
|
||||
|
||||
private static CommandOption shortOption(char name) {
|
||||
return shortOption(name, null);
|
||||
}
|
||||
|
||||
private static CommandOption shortOption(char name, ResolvableType type) {
|
||||
return CommandOption.of(new String[0], new Character[] { name }, "desc", type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionArity;
|
||||
import org.springframework.shell.command.CommandRegistration.TargetInfo.TargetType;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class CommandRegistrationTests extends AbstractCommandTests {
|
||||
|
||||
@Test
|
||||
public void testCommandMustBeSet() {
|
||||
assertThatThrownBy(() -> {
|
||||
CommandRegistration.builder().build();
|
||||
}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("command cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasics() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getGroup()).isNull();
|
||||
assertThat(registration.getInteractionMode()).isEqualTo(InteractionMode.ALL);
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.interactionMode(InteractionMode.NONINTERACTIVE)
|
||||
.group("fakegroup")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getInteractionMode()).isEqualTo(InteractionMode.NONINTERACTIVE);
|
||||
assertThat(registration.getGroup()).isEqualTo("fakegroup");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandStructures() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1", "command2")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1 command2");
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1 command2")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1 command2");
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command(" command1 command2 ")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1 command2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFunctionRegistration() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getTarget().getTargetType()).isEqualTo(TargetType.FUNCTION);
|
||||
assertThat(registration.getTarget().getFunction()).isNotNull();
|
||||
assertThat(registration.getTarget().getBean()).isNull();
|
||||
assertThat(registration.getTarget().getMethod()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumerRegistration() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getTarget().getTargetType()).isEqualTo(TargetType.CONSUMER);
|
||||
assertThat(registration.getTarget().getFunction()).isNull();
|
||||
assertThat(registration.getTarget().getConsumer()).isNotNull();
|
||||
assertThat(registration.getTarget().getBean()).isNull();
|
||||
assertThat(registration.getTarget().getMethod()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMethodRegistration() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getTarget().getTargetType()).isEqualTo(TargetType.METHOD);
|
||||
assertThat(registration.getTarget().getFunction()).isNull();
|
||||
assertThat(registration.getTarget().getBean()).isNotNull();
|
||||
assertThat(registration.getTarget().getMethod()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanUseOnlyOneTarget() {
|
||||
assertThatThrownBy(() -> {
|
||||
CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
}).isInstanceOf(IllegalStateException.class).hasMessageContaining("only one target can exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleFullRegistrationWithFunction() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getLongNames()).containsExactly("arg1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleFullRegistrationWithMethod() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.method(pojo1, "method3", String.class)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionWithType() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getShortNames()).containsExactly('v');
|
||||
assertThat(registration.getOptions().get(0).getType()).isEqualTo(ResolvableType.forType(boolean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionWithRequired() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.required(true)
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getShortNames()).containsExactly('v');
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isTrue();
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.required(false)
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isFalse();
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isFalse();
|
||||
|
||||
registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.required()
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).isRequired()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionWithDefaultValue() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.help("help")
|
||||
.withOption()
|
||||
.shortNames('v')
|
||||
.type(boolean.class)
|
||||
.description("some arg1")
|
||||
.defaultValue("defaultValue")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getHelp()).isEqualTo("help");
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getDefaultValue()).isEqualTo("defaultValue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionWithPositionValue() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.position(1)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg2")
|
||||
.and()
|
||||
.withTarget()
|
||||
.function(function1)
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getCommand()).isEqualTo("command1");
|
||||
assertThat(registration.getOptions()).hasSize(2);
|
||||
assertThat(registration.getOptions().get(0).getPosition()).isEqualTo(1);
|
||||
assertThat(registration.getOptions().get(1).getPosition()).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArityViaInts() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.arity(0, 0)
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
|
||||
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArityViaEnum() {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("command1")
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.arity(OptionArity.ZERO)
|
||||
.and()
|
||||
.withTarget()
|
||||
.consumer(ctx -> {})
|
||||
.and()
|
||||
.build();
|
||||
assertThat(registration.getOptions()).hasSize(1);
|
||||
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
|
||||
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,22 @@
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-asciidoc-snippets</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/refdocs/snippets/</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/test/java/org/springframework/shell/docs</directory>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
@@ -223,6 +239,7 @@
|
||||
<version>${project.version}</version>
|
||||
<projectName>${project.name}</projectName>
|
||||
<projectVersion>${project.version}</projectVersion>
|
||||
<snippets>${project.build.directory}/refdocs/snippets/</snippets>
|
||||
<allow-uri-read>true</allow-uri-read>
|
||||
<toclevels>4</toclevels>
|
||||
<numbered>true</numbered>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
=== Command Catalog
|
||||
`CommandCatalog` is an interface defining how command registrations exists in
|
||||
a shell application. It is possible to dynamically register and de-register
|
||||
commands which gives flexibility for a user cases where possible commands will
|
||||
come and go depending on a states shell is at.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandCatalogSnippets.java[tag=snippet1]
|
||||
----
|
||||
====
|
||||
|
||||
==== Command Resolver
|
||||
`CommandResolver` is an interface you can implement and define as a bean to dynamically
|
||||
resolve mappings from a command names to its `CommandRegistration` instances. Its use
|
||||
case looks something like:
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandCatalogSnippets.java[tag=snippet2]
|
||||
----
|
||||
====
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
Current limitation of a `CommandResolver` is that it is used every time commands are resolved.
|
||||
Thus it's adviced not to use it if command resolve call takes a long time as it would
|
||||
make shell feel sluggish.
|
||||
====
|
||||
|
||||
==== Command Catalog Customizer
|
||||
`CommandCatalogCustomizer` is an interface which can be used to customize a `CommandCatalog`.
|
||||
Its main use case is to modify catalog and within `spring-shell` _auto-configuration_ this
|
||||
interface is used to register existing `CommandRegistration` beans into a catalog.
|
||||
Its use case looks something like:
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandCatalogSnippets.java[tag=snippet3]
|
||||
----
|
||||
====
|
||||
|
||||
Create `CommandCatalogCustomizer` as a bean and `spring-shell` will handle rest.
|
||||
@@ -0,0 +1,20 @@
|
||||
=== Command Context
|
||||
`CommandContext` is an interface which gives access to a currently executing
|
||||
context. It can be used to get access to options:
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandContextSnippets.java[tag=snippet1]
|
||||
----
|
||||
====
|
||||
|
||||
If you need to print something into a shell you can get `Terminal`
|
||||
and use its writer to print something:
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandContextSnippets.java[tag=snippet2]
|
||||
----
|
||||
====
|
||||
@@ -0,0 +1,3 @@
|
||||
=== Command Execution
|
||||
When _command parsing_ has done its job togethere with resolving _command registration_, execution
|
||||
will do the hard work and execute a real user level code.
|
||||
@@ -0,0 +1,3 @@
|
||||
=== Command Parser
|
||||
Before a command can be executed we need to parse commands and options provided by a user. Parsing
|
||||
sits between _command registration_ and _command execution_.
|
||||
@@ -0,0 +1,106 @@
|
||||
[#appendix-tech-intro-registration]
|
||||
=== Command Registration
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
Defining a command registation is a first step to introduce a structure of a commands and its options
|
||||
and parameters. This is loosely decoupled what happens later like parsing command-line and executing
|
||||
actual target code. Essentially it is a definition of an command API shown to a user.
|
||||
|
||||
==== Commands
|
||||
Command in a `spring-shell` structure is defined as an array of commands. This will give
|
||||
you something like:
|
||||
|
||||
====
|
||||
[source, bash]
|
||||
----
|
||||
command1 sub1
|
||||
command2 sub1 subsub1
|
||||
command2 sub2 subsub1
|
||||
command2 sub2 subsub2
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
We don't currently support mapping commands to explicit parent if sub-commands are defined.
|
||||
For example there can't be `command1 sub1` and `command1 sub1 subsub1` registered.
|
||||
====
|
||||
|
||||
==== Interaction Mode
|
||||
Spring Shell has been designed to work on two modes one being interactive which essentially
|
||||
is a `REPL` where you have an active shell instance throughout commands and secondly
|
||||
non-interactive mode where commands are executed one by one from a command line.
|
||||
|
||||
Differentation between these modes are mostly around limitations what can be done
|
||||
in each mode as for example it would not be feasible to show what was a previous stacktrace
|
||||
of a command if shell is not alive anymore and generally things around information
|
||||
if shell is alive or not.
|
||||
|
||||
Also being on an active `REPL` session may provide more info about what user has been
|
||||
doing within an active session.
|
||||
|
||||
==== Options
|
||||
Options can be defined as long and short where prefixing is `--` and `-` respectively.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandRegistrationSnippets.java[tag=snippet1]
|
||||
----
|
||||
====
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandRegistrationSnippets.java[tag=snippet2]
|
||||
----
|
||||
====
|
||||
|
||||
==== Target
|
||||
Target defines what is an execution target of a command. It can be a _method_ in a `POJO`,
|
||||
`Consumer` or `Function`.
|
||||
|
||||
===== Method
|
||||
Using a `Method` is a way to define target as a method in an existing pojo.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandTargetSnippets.java[tag=snippet11]
|
||||
----
|
||||
====
|
||||
|
||||
Having existing class shown above you can then register its method.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandTargetSnippets.java[tag=snippet12]
|
||||
----
|
||||
====
|
||||
|
||||
===== Function
|
||||
Using a `Function` as a target gives a lot of flexibility to handle what
|
||||
happens in a command execution as you can handle many things manually using
|
||||
a `CommandContext` given to a `Function`. Return type from a `Function` is
|
||||
then what gets printed into a shell as a result.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandTargetSnippets.java[tag=snippet2]
|
||||
----
|
||||
====
|
||||
|
||||
===== Consumer
|
||||
Using a `Consumer` is basically same as `Function` with difference being
|
||||
that there is not return type. If you need to print something into a shell
|
||||
you can get a reference to a `Terminal` from a context and print something
|
||||
through it.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandTargetSnippets.java[tag=snippet3]
|
||||
----
|
||||
====
|
||||
@@ -0,0 +1,15 @@
|
||||
[appendix]
|
||||
[#appendix-tech-intro]
|
||||
== Techical Introduction
|
||||
This section contains information for a developers and others who would like to know more about how _spring-shell_
|
||||
internally works and what are its design decisions.
|
||||
|
||||
include::appendices-techical-intro-registration.adoc[]
|
||||
|
||||
include::appendices-techical-intro-parser.adoc[]
|
||||
|
||||
include::appendices-techical-intro-execution.adoc[]
|
||||
|
||||
include::appendices-techical-intro-commandcontext.adoc[]
|
||||
|
||||
include::appendices-techical-intro-commandcatalog.adoc[]
|
||||
1
spring-shell-docs/src/main/asciidoc/appendices.adoc
Normal file
1
spring-shell-docs/src/main/asciidoc/appendices.adoc
Normal file
@@ -0,0 +1 @@
|
||||
include::appendices-techical-intro.adoc[]
|
||||
@@ -20,3 +20,5 @@ include::introduction.adoc[]
|
||||
include::getting-started.adoc[]
|
||||
|
||||
include::using-shell.adoc[]
|
||||
|
||||
include::appendices.adoc[]
|
||||
|
||||
14
spring-shell-docs/src/main/asciidoc/using-shell-basics.adoc
Normal file
14
spring-shell-docs/src/main/asciidoc/using-shell-basics.adoc
Normal file
@@ -0,0 +1,14 @@
|
||||
[[using-shell-basics]]
|
||||
=== Basics
|
||||
You are here to learn basics of a _spring shell_. Before going forward to define actual _commands_ and _options_
|
||||
lets take this moment to go trough some fundamental concepts of a _spring shell_.
|
||||
|
||||
Essentially few things needs to happen before you have a working _spring shell_ app:
|
||||
|
||||
- Create a _spring boot_ application
|
||||
- Define commands and its option
|
||||
- Package an application
|
||||
- Execute either interactively or non-interactively
|
||||
|
||||
You will get a full working _spring shell_ application without defining any user level commands
|
||||
as some basic build-in commands are provided out of a box like `help` and `history`.
|
||||
@@ -1,13 +1,21 @@
|
||||
=== Writing Your Own Commands
|
||||
=== Define a Command
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
The way Spring Shell decides to turn a method into an actual shell command is entirely pluggable
|
||||
(see <<extending-spring-shell>>). However, as of Spring Shell 2.x, the recommended way to write commands
|
||||
is to use the new API described in this section (the standard API).
|
||||
In this section we go through an actual command registration and leave command options
|
||||
and execution later in a documentation. More detailed info can be found from
|
||||
<<appendix-tech-intro-registration>>.
|
||||
|
||||
There are two different ways to define a command. Firstly through an annotation model and
|
||||
secondly through programmatic model. Annotation model if where you define your methods
|
||||
in a class and annotate class and methods with a spesific annotations. Programmatic model
|
||||
is where things are done on a more low level ways by defining command registrations either
|
||||
as beans or registering those with a command catalog dynamically.
|
||||
|
||||
==== Annotation Model
|
||||
When you use the standard API, methods on beans are turned into executable commands, provided that:
|
||||
|
||||
* The bean class bears the `@ShellComponent` annotation. This is used to restrict the set of beans that
|
||||
are considered.
|
||||
* The bean class bears the `@ShellComponent` annotation. This is used to restrict the set of beans
|
||||
that are considered.
|
||||
* The method bears the `@ShellMethod` annotation.
|
||||
|
||||
[TIP]
|
||||
@@ -18,17 +26,22 @@ you can used it in addition to the filtering mechanism to declare beans (for exa
|
||||
You can customize the name of the created bean by using the `value` attribute of the annotation.
|
||||
====
|
||||
|
||||
[[documenting-the-command]]
|
||||
==== Documenting the Command
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/AnnotationRegistrationSnippets.java[tag=snippet1]
|
||||
----
|
||||
====
|
||||
|
||||
The only required attribute of the `@ShellMethod` annotation is its `value` attribute, which should have
|
||||
a short, one-sentence, description of what the command does. This lets your users
|
||||
get consistent help about your commands without having to leave the shell (see <<help-command>>).
|
||||
|
||||
NOTE: The description of your command should be short -- no more than one or two sentences. For better
|
||||
[NOTE]
|
||||
====
|
||||
The description of your command should be short -- no more than one or two sentences. For better
|
||||
consistency, it should starts with a capital letter and end with a period.
|
||||
|
||||
==== Customizing the Command Name(s)
|
||||
====
|
||||
|
||||
By default, there is no need to specify the key for your command (that is, the word(s) that should be used
|
||||
to invoke it in the shell). The name of the method is used as the command key, turning camelCase names into
|
||||
@@ -37,19 +50,31 @@ dashed, gnu-style, names (that is, `sayHello()` becomes `say-hello`).
|
||||
You can, however, explicitly set the command key, by using the `key` attribute of the annotation:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
@ShellMethod(value = "Add numbers.", key = "sum")
|
||||
public int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
include::{snippets}/AnnotationRegistrationSnippets.java[tag=snippet2]
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: The `key` attribute accepts multiple values.
|
||||
[NOTE]
|
||||
====
|
||||
The `key` attribute accepts multiple values.
|
||||
If you set multiple keys for a single method, the command is registered with those different aliases.
|
||||
====
|
||||
|
||||
TIP: The command key can contain pretty much any character, including spaces. When coming up with names though,
|
||||
[TIP]
|
||||
====
|
||||
The command key can contain pretty much any character, including spaces. When coming up with names though,
|
||||
keep in mind that consistency is often appreciated by users (that is, you should avoid mixing dashed-names with
|
||||
spaced names and other inconsistencies).
|
||||
====
|
||||
|
||||
==== Programmatic Model
|
||||
`CommandRegistration` can be defined as a `@Bean` and it's automatically registered.
|
||||
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/CommandRegistrationBeanSnippets.java[tag=snippet1]
|
||||
----
|
||||
====
|
||||
@@ -1,5 +1,6 @@
|
||||
[[uicomponents]]
|
||||
=== UI Components
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
Starting from _2.1.x_ there is a new component model which provides
|
||||
easier way to create higher level user interaction for usual use cases
|
||||
@@ -32,49 +33,18 @@ via code then gives you flexibility to do whatever you need.
|
||||
Programmatic way to render is simple as to create a `Function`:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
class StringInputCustomRenderer implements Function<StringInputContext, List<AttributedString>> {
|
||||
@Override
|
||||
public List<AttributedString> apply(StringInputContext context) {
|
||||
AttributedStringBuilder builder = new AttributedStringBuilder();
|
||||
builder.append(context.getName());
|
||||
builder.append(" ");
|
||||
if (context.getResultValue() != null) {
|
||||
builder.append(context.getResultValue());
|
||||
}
|
||||
else {
|
||||
String input = context.getInput();
|
||||
if (StringUtils.hasText(input)) {
|
||||
builder.append(input);
|
||||
}
|
||||
else {
|
||||
builder.append("[Default " + context.getDefaultValue() + "]");
|
||||
}
|
||||
}
|
||||
return Arrays.asList(builder.toAttributedString());
|
||||
}
|
||||
}
|
||||
include::{snippets}/UiComponentSnippets.java[tag=snippet1]
|
||||
----
|
||||
====
|
||||
|
||||
And then hook it with a component:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
@ShellMethod(key = "component stringcustom", value = "String input", group = "Components")
|
||||
public String stringInputCustom(boolean mask) {
|
||||
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue",
|
||||
new StringInputCustomRenderer());
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
if (mask) {
|
||||
component.setMaskCharater('*');
|
||||
}
|
||||
StringInputContext context = component.run(StringInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
include::{snippets}/UiComponentSnippets.java[tag=snippet2]
|
||||
----
|
||||
====
|
||||
|
||||
@@ -147,26 +117,13 @@ Used to ask a simple text input from a user, optionally masking values
|
||||
if content contains something sensitive.
|
||||
|
||||
====
|
||||
[source, java]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component string", value = "String input", group = "Components")
|
||||
public String stringInput(boolean mask) {
|
||||
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
if (mask) {
|
||||
component.setMaskCharater('*');
|
||||
}
|
||||
StringInputContext context = component.run(StringInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
}
|
||||
include::{snippets}/UiComponentSnippets.java[tag=snippet3]
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
image::images/component-text-input-1.svg[text input]
|
||||
|
||||
Context object is `StringInputContext`.
|
||||
@@ -200,20 +157,9 @@ Context object is `StringInputContext`.
|
||||
Used to ask a `Path` from a user and gives additional info about a path itself.
|
||||
|
||||
====
|
||||
[source, java]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component path", value = "Path input", group = "Components")
|
||||
public String pathInput() {
|
||||
PathInput component = new PathInput(getTerminal(), "Enter value");
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
PathInputContext context = component.run(PathInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
}
|
||||
include::{snippets}/UiComponentSnippets.java[tag=snippet4]
|
||||
----
|
||||
====
|
||||
|
||||
@@ -236,20 +182,9 @@ Used to ask a simple confirmation from a user and essentially is
|
||||
yes/no question.
|
||||
|
||||
====
|
||||
[source, java]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
|
||||
public String confirmationInput(boolean no) {
|
||||
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
ConfirmationInputContext context = component.run(ConfirmationInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
}
|
||||
include::{snippets}/UiComponentSnippets.java[tag=snippet5]
|
||||
----
|
||||
====
|
||||
|
||||
@@ -275,26 +210,9 @@ Used to ask an item from a list and is essentially similar to simple
|
||||
dropbox implementation.
|
||||
|
||||
====
|
||||
[source, java]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
|
||||
public String singleSelector() {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
items.add(SelectorItem.of("key1", "value1"));
|
||||
items.add(SelectorItem.of("key2", "value2"));
|
||||
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
SingleItemSelectorContext<String, SelectorItem<String>> context = component
|
||||
.run(SingleItemSelectorContext.empty());
|
||||
String result = context.getResultItem().flatMap(si -> Optional.ofNullable(si.getItem())).get();
|
||||
return "Got value " + result;
|
||||
}
|
||||
}
|
||||
include::{snippets}/UiComponentSnippets.java[tag=snippet6]
|
||||
----
|
||||
====
|
||||
|
||||
@@ -322,29 +240,9 @@ Context object is `SingleItemSelectorContext`.
|
||||
Used to ask an items from a list.
|
||||
|
||||
====
|
||||
[source, java]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
|
||||
public String multiSelector() {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
items.add(SelectorItem.of("key1", "value1"));
|
||||
items.add(SelectorItem.of("key2", "value2", false));
|
||||
items.add(SelectorItem.of("key3", "value3"));
|
||||
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
MultiItemSelectorContext<String, SelectorItem<String>> context = component
|
||||
.run(MultiItemSelectorContext.empty());
|
||||
String result = context.getResultItems().stream()
|
||||
.map(si -> si.getItem())
|
||||
.collect(Collectors.joining(","));
|
||||
return "Got value " + result;
|
||||
}
|
||||
}
|
||||
include::{snippets}/UiComponentSnippets.java[tag=snippet7]
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ more relevant on a java space. Moving to new major version also allows
|
||||
us to clean up codebase and make some needed breaking changes.
|
||||
====
|
||||
|
||||
include::using-shell-write-command.adoc[]
|
||||
include::using-shell-basics.adoc[]
|
||||
|
||||
include::using-shell-define-command.adoc[]
|
||||
|
||||
include::using-shell-invoke-command.adoc[]
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.springframework.shell.docs;
|
||||
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
|
||||
public class AnnotationRegistrationSnippets {
|
||||
|
||||
// tag::snippet1[]
|
||||
@ShellComponent
|
||||
static class MyCommands {
|
||||
|
||||
@ShellMethod
|
||||
public void mycommand() {
|
||||
}
|
||||
}
|
||||
// end::snippet1[]
|
||||
|
||||
static class Dump1 {
|
||||
|
||||
// tag::snippet2[]
|
||||
@ShellMethod(value = "Add numbers.", key = "sum")
|
||||
public int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
// end::snippet2[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.springframework.shell.docs;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandCatalogCustomizer;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.CommandResolver;
|
||||
|
||||
public class CommandCatalogSnippets {
|
||||
|
||||
CommandCatalog catalog = CommandCatalog.of();
|
||||
|
||||
void dump1() {
|
||||
// tag::snippet1[]
|
||||
CommandRegistration registration = CommandRegistration.builder().build();
|
||||
catalog.register(registration);
|
||||
// end::snippet1[]
|
||||
}
|
||||
|
||||
// tag::snippet2[]
|
||||
static class CustomCommandResolver implements CommandResolver {
|
||||
List<CommandRegistration> registrations = new ArrayList<>();
|
||||
|
||||
CustomCommandResolver() {
|
||||
CommandRegistration resolved = CommandRegistration.builder()
|
||||
.command("resolve command")
|
||||
.build();
|
||||
registrations.add(resolved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandRegistration> resolve() {
|
||||
return registrations;
|
||||
}
|
||||
}
|
||||
// end::snippet2[]
|
||||
|
||||
// tag::snippet3[]
|
||||
static class CustomCommandCatalogCustomizer implements CommandCatalogCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(CommandCatalog commandCatalog) {
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("resolve command")
|
||||
.build();
|
||||
commandCatalog.register(registration);
|
||||
}
|
||||
}
|
||||
// end::snippet3[]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.springframework.shell.docs;
|
||||
|
||||
import javax.validation.constraints.Null;
|
||||
|
||||
import org.springframework.shell.command.CommandContext;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CommandContextSnippets {
|
||||
|
||||
CommandContext ctx = CommandContext.of(null, null, null);
|
||||
|
||||
void dump1() {
|
||||
// tag::snippet1[]
|
||||
String arg = ctx.getOptionValue("arg");
|
||||
// end::snippet1[]
|
||||
}
|
||||
|
||||
void dump2() {
|
||||
// tag::snippet2[]
|
||||
ctx.getTerminal().writer().println("hi");
|
||||
// end::snippet2[]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.springframework.shell.docs;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
public class CommandRegistrationBeanSnippets {
|
||||
|
||||
// tag::snippet1[]
|
||||
@Bean
|
||||
CommandRegistration commandRegistration() {
|
||||
return CommandRegistration.builder()
|
||||
.command("mycommand")
|
||||
.build();
|
||||
}
|
||||
// end::snippet1[]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.springframework.shell.docs;
|
||||
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
public class CommandRegistrationSnippets {
|
||||
|
||||
void dump1() {
|
||||
// tag::snippet1[]
|
||||
CommandRegistration.builder()
|
||||
.withOption()
|
||||
.longNames("myopt")
|
||||
.and()
|
||||
.build();
|
||||
// end::snippet1[]
|
||||
}
|
||||
|
||||
void dump2() {
|
||||
// tag::snippet2[]
|
||||
CommandRegistration.builder()
|
||||
.withOption()
|
||||
.shortNames('s')
|
||||
.and()
|
||||
.build();
|
||||
// end::snippet2[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.springframework.shell.docs;
|
||||
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
public class CommandTargetSnippets {
|
||||
|
||||
// tag::snippet11[]
|
||||
public static class CommandPojo {
|
||||
|
||||
String command(String arg) {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
// end::snippet11[]
|
||||
|
||||
void dump1() {
|
||||
// tag::snippet12[]
|
||||
CommandPojo pojo = new CommandPojo();
|
||||
CommandRegistration.builder()
|
||||
.command("command")
|
||||
.withTarget()
|
||||
.method(pojo, "command")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg")
|
||||
.and()
|
||||
.build();
|
||||
// end::snippet12[]
|
||||
}
|
||||
|
||||
void dump2() {
|
||||
// tag::snippet2[]
|
||||
CommandRegistration.builder()
|
||||
.command("command")
|
||||
.withTarget()
|
||||
.function(ctx -> {
|
||||
String arg = ctx.getOptionValue("arg");
|
||||
return String.format("hi, arg value is '%s'", arg);
|
||||
})
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg")
|
||||
.and()
|
||||
.build();
|
||||
// end::snippet2[]
|
||||
}
|
||||
|
||||
void dump3() {
|
||||
// tag::snippet3[]
|
||||
CommandRegistration.builder()
|
||||
.command("command")
|
||||
.withTarget()
|
||||
.consumer(ctx -> {
|
||||
String arg = ctx.getOptionValue("arg");
|
||||
ctx.getTerminal().writer()
|
||||
.println(String.format("hi, arg value is '%s'", arg));
|
||||
})
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg")
|
||||
.and()
|
||||
.build();
|
||||
// end::snippet3[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package org.springframework.shell.docs;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
|
||||
import org.springframework.shell.component.ConfirmationInput;
|
||||
import org.springframework.shell.component.MultiItemSelector;
|
||||
import org.springframework.shell.component.PathInput;
|
||||
import org.springframework.shell.component.SingleItemSelector;
|
||||
import org.springframework.shell.component.StringInput;
|
||||
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
|
||||
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
|
||||
import org.springframework.shell.component.PathInput.PathInputContext;
|
||||
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
|
||||
import org.springframework.shell.component.StringInput.StringInputContext;
|
||||
import org.springframework.shell.component.support.SelectorItem;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public class UiComponentSnippets {
|
||||
|
||||
// tag::snippet1[]
|
||||
class StringInputCustomRenderer implements Function<StringInputContext, List<AttributedString>> {
|
||||
@Override
|
||||
public List<AttributedString> apply(StringInputContext context) {
|
||||
AttributedStringBuilder builder = new AttributedStringBuilder();
|
||||
builder.append(context.getName());
|
||||
builder.append(" ");
|
||||
if (context.getResultValue() != null) {
|
||||
builder.append(context.getResultValue());
|
||||
}
|
||||
else {
|
||||
String input = context.getInput();
|
||||
if (StringUtils.hasText(input)) {
|
||||
builder.append(input);
|
||||
}
|
||||
else {
|
||||
builder.append("[Default " + context.getDefaultValue() + "]");
|
||||
}
|
||||
}
|
||||
return Arrays.asList(builder.toAttributedString());
|
||||
}
|
||||
}
|
||||
// end::snippet1[]
|
||||
|
||||
class Dump1 extends AbstractShellComponent {
|
||||
// tag::snippet2[]
|
||||
@ShellMethod(key = "component stringcustom", value = "String input", group = "Components")
|
||||
public String stringInputCustom(boolean mask) {
|
||||
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue",
|
||||
new StringInputCustomRenderer());
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
if (mask) {
|
||||
component.setMaskCharater('*');
|
||||
}
|
||||
StringInputContext context = component.run(StringInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
// end::snippet2[]
|
||||
}
|
||||
|
||||
class Dump2 {
|
||||
// tag::snippet3[]
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component string", value = "String input", group = "Components")
|
||||
public String stringInput(boolean mask) {
|
||||
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
if (mask) {
|
||||
component.setMaskCharater('*');
|
||||
}
|
||||
StringInputContext context = component.run(StringInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
}
|
||||
// end::snippet3[]
|
||||
}
|
||||
|
||||
class Dump3 {
|
||||
// tag::snippet4[]
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component path", value = "Path input", group = "Components")
|
||||
public String pathInput() {
|
||||
PathInput component = new PathInput(getTerminal(), "Enter value");
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
PathInputContext context = component.run(PathInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
}
|
||||
// end::snippet4[]
|
||||
}
|
||||
|
||||
class Dump4 {
|
||||
// tag::snippet5[]
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
|
||||
public String confirmationInput(boolean no) {
|
||||
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
ConfirmationInputContext context = component.run(ConfirmationInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
}
|
||||
// end::snippet5[]
|
||||
}
|
||||
|
||||
class Dump5 {
|
||||
// tag::snippet6[]
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
|
||||
public String singleSelector() {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
items.add(SelectorItem.of("key1", "value1"));
|
||||
items.add(SelectorItem.of("key2", "value2"));
|
||||
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
SingleItemSelectorContext<String, SelectorItem<String>> context = component
|
||||
.run(SingleItemSelectorContext.empty());
|
||||
String result = context.getResultItem().flatMap(si -> Optional.ofNullable(si.getItem())).get();
|
||||
return "Got value " + result;
|
||||
}
|
||||
}
|
||||
// end::snippet6[]
|
||||
}
|
||||
|
||||
class Dump6 {
|
||||
// tag::snippet7[]
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
|
||||
public String multiSelector() {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
items.add(SelectorItem.of("key1", "value1"));
|
||||
items.add(SelectorItem.of("key2", "value2", false));
|
||||
items.add(SelectorItem.of("key3", "value3"));
|
||||
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
component.setResourceLoader(getResourceLoader());
|
||||
component.setTemplateExecutor(getTemplateExecutor());
|
||||
MultiItemSelectorContext<String, SelectorItem<String>> context = component
|
||||
.run(MultiItemSelectorContext.empty());
|
||||
String result = context.getResultItems().stream()
|
||||
.map(si -> si.getItem())
|
||||
.collect(Collectors.joining(","));
|
||||
return "Got value " + result;
|
||||
}
|
||||
}
|
||||
// end::snippet7[]
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.shell.standard.commands;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
@@ -51,8 +49,7 @@ public class Completion extends AbstractShellComponent {
|
||||
|
||||
@ShellMethod(key = "completion bash", value = "Generate bash completion script")
|
||||
public String bash() {
|
||||
BashCompletions bashCompletions = new BashCompletions(resourceLoader, getCommandRegistry(),
|
||||
getParameterResolver().collect(Collectors.toList()));
|
||||
BashCompletions bashCompletions = new BashCompletions(resourceLoader, getCommandCatalog());
|
||||
return bashCompletions.generate(rootCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 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,15 +13,14 @@
|
||||
* 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.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeMap;
|
||||
@@ -37,24 +36,26 @@ import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandOption;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
import org.springframework.shell.standard.CommandValueProvider;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
import static java.util.stream.Collectors.mapping;
|
||||
import static java.util.stream.Collectors.toCollection;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
/**
|
||||
* A command to display help about all available commands.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@ShellComponent
|
||||
public class Help extends AbstractShellComponent {
|
||||
@@ -91,7 +92,7 @@ public class Help extends AbstractShellComponent {
|
||||
@ShellMethod(value = "Display help about available commands.", prefix = "-")
|
||||
public CharSequence help(
|
||||
@ShellOption(defaultValue = ShellOption.NULL, valueProvider = CommandValueProvider.class, value = { "-C",
|
||||
"--command" }, help = "The command to obtain help for.") String command)
|
||||
"--command" }, help = "The command to obtain help for.", arity = Integer.MAX_VALUE) String command)
|
||||
throws IOException {
|
||||
if (command == null) {
|
||||
return listCommands();
|
||||
@@ -116,16 +117,17 @@ public class Help extends AbstractShellComponent {
|
||||
* Return a description of a specific command. Uses a layout inspired by *nix man pages.
|
||||
*/
|
||||
private CharSequence documentCommand(String command) {
|
||||
MethodTarget methodTarget = getCommandRegistry().listCommands().get(command);
|
||||
if (methodTarget == null) {
|
||||
Map<String, CommandRegistration> registrations = getCommandCatalog().getRegistrations();
|
||||
CommandRegistration registration = registrations.get(command);
|
||||
if (registration == null) {
|
||||
throw new IllegalArgumentException("Unknown command '" + command + "'");
|
||||
}
|
||||
|
||||
AttributedStringBuilder result = new AttributedStringBuilder().append("\n\n");
|
||||
List<ParameterDescription> parameterDescriptions = getParameterDescriptions(methodTarget);
|
||||
List<ParameterDescription> parameterDescriptions = getParameterDescriptions(registration);
|
||||
|
||||
// NAME
|
||||
documentCommandName(result, command, methodTarget.getHelp());
|
||||
documentCommandName(result, command, registration.getHelp());
|
||||
|
||||
// SYNOPSYS
|
||||
documentSynopsys(result, command, parameterDescriptions);
|
||||
@@ -134,10 +136,10 @@ public class Help extends AbstractShellComponent {
|
||||
documentOptions(result, parameterDescriptions);
|
||||
|
||||
// ALSO KNOWN AS
|
||||
documentAliases(result, command, methodTarget);
|
||||
documentAliases(result, command, registrations, registration);
|
||||
|
||||
// AVAILABILITY
|
||||
documentAvailability(result, methodTarget);
|
||||
documentAvailability(result, registration);
|
||||
|
||||
result.append("\n");
|
||||
return result;
|
||||
@@ -242,12 +244,13 @@ public class Help extends AbstractShellComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private void documentAliases(AttributedStringBuilder result, String command, MethodTarget methodTarget) {
|
||||
Set<String> aliases = getCommandRegistry().listCommands().entrySet().stream()
|
||||
.filter(e -> e.getValue().equals(methodTarget))
|
||||
.map(Map.Entry::getKey)
|
||||
.filter(c -> !command.equals(c))
|
||||
.collect(toCollection(TreeSet::new));
|
||||
private void documentAliases(AttributedStringBuilder result, String command,
|
||||
Map<String, CommandRegistration> registrations, CommandRegistration registration) {
|
||||
List<String> aliases = registrations.entrySet().stream()
|
||||
.filter(e -> e.getValue().equals(registration))
|
||||
.map(Map.Entry::getKey)
|
||||
.filter(c -> !command.equals(c))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!aliases.isEmpty()) {
|
||||
result.append("ALSO KNOWN AS", AttributedStyle.BOLD).append("\n");
|
||||
@@ -257,8 +260,8 @@ public class Help extends AbstractShellComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private void documentAvailability(AttributedStringBuilder result, MethodTarget methodTarget) {
|
||||
Availability availability = methodTarget.getAvailability();
|
||||
private void documentAvailability(AttributedStringBuilder result, CommandRegistration registration) {
|
||||
Availability availability = registration.getAvailability();
|
||||
if (!availability.isAvailable()) {
|
||||
result.append("CURRENTLY UNAVAILABLE", AttributedStyle.BOLD).append("\n");
|
||||
result.append('\t').append("This command is currently not available because ")
|
||||
@@ -272,20 +275,21 @@ public class Help extends AbstractShellComponent {
|
||||
}
|
||||
|
||||
private CharSequence listCommands() {
|
||||
Map<String, MethodTarget> commandsByName = getCommandRegistry().listCommands();
|
||||
|
||||
AttributedStringBuilder result = new AttributedStringBuilder();
|
||||
result.append("AVAILABLE COMMANDS\n\n", AttributedStyle.BOLD);
|
||||
|
||||
SortedMap<String, Map<String, MethodTarget>> commandsByGroupAndName = commandsByName.entrySet().stream()
|
||||
.collect(groupingBy(e -> e.getValue().getGroup(), TreeMap::new, // group by and sort by command group
|
||||
toMap(Entry::getKey, Entry::getValue)));
|
||||
// display groups, sorted alphabetically, "Default" first
|
||||
SortedMap<String, Map<String, CommandRegistration>> commandsByGroupAndName = getCommandCatalog().getRegistrations().entrySet().stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
e -> StringUtils.hasText(e.getValue().getGroup()) ? e.getValue().getGroup() : "",
|
||||
TreeMap::new,
|
||||
Collectors.toMap(Entry::getKey, Entry::getValue)
|
||||
));
|
||||
|
||||
commandsByGroupAndName.forEach((group, commandsInGroup) -> {
|
||||
if (showGroups) {
|
||||
result.append("".equals(group) ? "Default" : group, AttributedStyle.BOLD).append('\n');
|
||||
}
|
||||
Map<MethodTarget, SortedSet<String>> commandNamesByMethod = commandsInGroup.entrySet().stream()
|
||||
Map<CommandRegistration, SortedSet<String>> commandNamesByMethod = commandsInGroup.entrySet().stream()
|
||||
.collect(groupingBy(Entry::getValue, // group by command method
|
||||
mapping(Entry::getKey, toCollection(TreeSet::new)))); // sort command names
|
||||
// display commands, sorted alphabetically by their first alias
|
||||
@@ -304,19 +308,15 @@ public class Help extends AbstractShellComponent {
|
||||
}
|
||||
});
|
||||
|
||||
if (commandsByName.values().stream().distinct().anyMatch(m -> !isAvailable(m))) {
|
||||
result.append("Commands marked with (*) are currently unavailable.\nType `help <command>` to learn more.\n\n");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Comparator<Entry<MethodTarget, SortedSet<String>>> sortByFirstCommandName() {
|
||||
private Comparator<Entry<CommandRegistration, SortedSet<String>>> sortByFirstCommandName() {
|
||||
return Comparator.comparing(e -> e.getValue().first());
|
||||
}
|
||||
|
||||
private boolean isAvailable(MethodTarget methodTarget) {
|
||||
return methodTarget.getAvailability().isAvailable();
|
||||
private boolean isAvailable(CommandRegistration methodTarget) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void appendUnderlinedFormal(AttributedStringBuilder result, ParameterDescription description) {
|
||||
@@ -330,12 +330,50 @@ public class Help extends AbstractShellComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private List<ParameterDescription> getParameterDescriptions(MethodTarget methodTarget) {
|
||||
return Utils.createMethodParameters(methodTarget.getMethod())
|
||||
.flatMap(mp -> getParameterResolver().filter(pr -> pr.supports(mp)).limit(1L)
|
||||
.flatMap(pr -> pr.describe(mp)))
|
||||
.collect(Collectors.toList());
|
||||
private List<ParameterDescription> getParameterDescriptions(CommandRegistration registration) {
|
||||
List<CommandOption> options = registration.getOptions();
|
||||
List<ParameterDescription> descriptions = new ArrayList<>();
|
||||
|
||||
for (CommandOption option : options) {
|
||||
|
||||
ParameterDescription description = new ParameterDescription();
|
||||
if (option.getType() != null) {
|
||||
description.type(option.getType().toString());
|
||||
description.formal(option.getType().toClass().getSimpleName());
|
||||
}
|
||||
else {
|
||||
description.formal("");
|
||||
}
|
||||
description.help(option.getDescription());
|
||||
description.mandatoryKey(option.isRequired());
|
||||
if (option.getType() != null && option.getType().isAssignableFrom(boolean.class)) {
|
||||
description.defaultValue("false");
|
||||
}
|
||||
else {
|
||||
description.defaultValue(option.getDefaultValue());
|
||||
}
|
||||
|
||||
List<String> keys = new ArrayList<>();
|
||||
if (option.getLongNames() != null) {
|
||||
for (String ln : option.getLongNames()) {
|
||||
keys.add("--" + ln);
|
||||
}
|
||||
}
|
||||
if (option.getShortNames() != null) {
|
||||
for (Character sn : option.getShortNames()) {
|
||||
keys.add("-" + String.valueOf(sn));
|
||||
}
|
||||
}
|
||||
description.keys(keys);
|
||||
|
||||
descriptions.add(description);
|
||||
}
|
||||
|
||||
// return Utils.createMethodParameters(registration.getTarget().getMethod())
|
||||
// .flatMap(mp -> getParameterResolver().filter(pr -> pr.supports(mp)).limit(1L)
|
||||
// .flatMap(pr -> pr.describe(mp)))
|
||||
// .collect(Collectors.toList());
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
private static class DummyContext implements MessageInterpolator.Context {
|
||||
@@ -361,5 +399,4 @@ public class Help extends AbstractShellComponent {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
@@ -26,9 +26,9 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.Shell;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
import org.springframework.shell.style.TemplateExecutor;
|
||||
import org.springframework.shell.style.ThemeResolver;
|
||||
|
||||
@@ -47,9 +47,9 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
|
||||
private ObjectProvider<Terminal> terminalProvider;
|
||||
|
||||
private ObjectProvider<CommandRegistry> commandRegistryProvider;
|
||||
private ObjectProvider<CommandCatalog> commandCatalogProvider;
|
||||
|
||||
private ObjectProvider<ParameterResolver> parameterResolverProvider;
|
||||
private ObjectProvider<CompletionResolver> completionResolverProvider;
|
||||
|
||||
private ObjectProvider<TemplateExecutor> templateExecutorProvider;
|
||||
|
||||
@@ -69,8 +69,8 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
shellProvider = applicationContext.getBeanProvider(Shell.class);
|
||||
terminalProvider = applicationContext.getBeanProvider(Terminal.class);
|
||||
commandRegistryProvider = applicationContext.getBeanProvider(CommandRegistry.class);
|
||||
parameterResolverProvider = applicationContext.getBeanProvider(ParameterResolver.class);
|
||||
commandCatalogProvider = applicationContext.getBeanProvider(CommandCatalog.class);
|
||||
completionResolverProvider = applicationContext.getBeanProvider(CompletionResolver.class);
|
||||
templateExecutorProvider = applicationContext.getBeanProvider(TemplateExecutor.class);
|
||||
themeResolverProvider = applicationContext.getBeanProvider(ThemeResolver.class);
|
||||
}
|
||||
@@ -91,12 +91,12 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
return terminalProvider.getObject();
|
||||
}
|
||||
|
||||
protected CommandRegistry getCommandRegistry() {
|
||||
return commandRegistryProvider.getObject();
|
||||
protected CommandCatalog getCommandCatalog() {
|
||||
return commandCatalogProvider.getObject();
|
||||
}
|
||||
|
||||
protected Stream<ParameterResolver> getParameterResolver() {
|
||||
return parameterResolverProvider.orderedStream();
|
||||
protected Stream<CompletionResolver> getCompletionResolver() {
|
||||
return completionResolverProvider.orderedStream();
|
||||
}
|
||||
|
||||
protected TemplateExecutor getTemplateExecutor() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 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.
|
||||
@@ -20,9 +20,9 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
|
||||
/**
|
||||
* A {@link ValueProvider} that can be used to auto-complete names of shell commands.
|
||||
@@ -31,15 +31,15 @@ import org.springframework.shell.CompletionProposal;
|
||||
*/
|
||||
public class CommandValueProvider extends ValueProviderSupport {
|
||||
|
||||
private final CommandRegistry commandRegistry;
|
||||
private final CommandCatalog commandRegistry;
|
||||
|
||||
public CommandValueProvider(CommandRegistry commandRegistry) {
|
||||
public CommandValueProvider(CommandCatalog commandRegistry) {
|
||||
this.commandRegistry = commandRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompletionProposal> complete(MethodParameter parameter, CompletionContext completionContext, String[] hints) {
|
||||
return commandRegistry.listCommands().keySet().stream()
|
||||
return commandRegistry.getRegistrations().keySet().stream()
|
||||
.map(CompletionProposal::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.standard;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandlingException;
|
||||
import org.springframework.shell.support.AbstractArgumentMethodArgumentResolver;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Resolver for {@link ShellOption @ShellOption} arguments.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ShellOptionMethodArgumentResolver extends AbstractArgumentMethodArgumentResolver {
|
||||
|
||||
public ShellOptionMethodArgumentResolver(ConversionService conversionService,
|
||||
@Nullable ConfigurableBeanFactory beanFactory) {
|
||||
super(conversionService, beanFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(ShellOption.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
|
||||
ShellOption annot = parameter.getParameterAnnotation(ShellOption.class);
|
||||
Assert.state(annot != null, "No ShellOption annotation");
|
||||
List<String> names = Arrays.stream(annot.value()).map(v -> StringUtils.trimLeadingCharacter(v, '-')).collect(Collectors.toList());
|
||||
return new HeaderNamedValueInfo(annot, names);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, List<String> names)
|
||||
throws Exception {
|
||||
for (String name : names) {
|
||||
if (message.getHeaders().containsKey(ARGUMENT_PREFIX + name)) {
|
||||
return message.getHeaders().get(ARGUMENT_PREFIX + name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMissingValue(List<String> headerName, MethodParameter parameter, Message<?> message) {
|
||||
throw new MessageHandlingException(message,
|
||||
"Missing headers '" + StringUtils.collectionToCommaDelimitedString(headerName)
|
||||
+ "' for method parameter type [" + parameter.getParameterType() + "]");
|
||||
}
|
||||
|
||||
private static final class HeaderNamedValueInfo extends NamedValueInfo {
|
||||
|
||||
private HeaderNamedValueInfo(ShellOption annotation, List<String> names) {
|
||||
super(names, false, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,21 +26,27 @@ import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.Command;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.MethodTargetRegistrar;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.CommandRegistration.Builder;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionSpec;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.springframework.util.StringUtils.collectionToDelimitedString;
|
||||
|
||||
/**
|
||||
* The standard implementation of {@link MethodTargetRegistrar} for new shell
|
||||
* applications, resolves methods annotated with {@link ShellMethod} on
|
||||
@@ -49,20 +55,20 @@ import static org.springframework.util.StringUtils.collectionToDelimitedString;
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
* @author Camilo Gonzalez
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, ApplicationContextAware {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(StandardMethodTargetRegistrar.class);
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private Map<String, MethodTarget> commands = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ConfigurableCommandRegistry registry) {
|
||||
public void register(CommandCatalog registry) {
|
||||
Map<String, Object> commandBeans = applicationContext.getBeansWithAnnotation(ShellComponent.class);
|
||||
for (Object bean : commandBeans.values()) {
|
||||
Class<?> clazz = bean.getClass();
|
||||
@@ -74,11 +80,78 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App
|
||||
}
|
||||
String group = getOrInferGroup(method);
|
||||
for (String key : keys) {
|
||||
log.debug("Registering with keys='{}' key='{}'", keys, key);
|
||||
Supplier<Availability> availabilityIndicator = findAvailabilityIndicator(keys, bean, method);
|
||||
MethodTarget target = new MethodTarget(method, bean, new Command.Help(shellMapping.value(), group),
|
||||
availabilityIndicator, shellMapping.interactionMode());
|
||||
registry.register(key, target);
|
||||
commands.put(key, target);
|
||||
|
||||
Builder builder = CommandRegistration.builder()
|
||||
.command(key)
|
||||
.group(group)
|
||||
.help(shellMapping.value())
|
||||
.interactionMode(shellMapping.interactionMode())
|
||||
.availability(availabilityIndicator);
|
||||
|
||||
InvocableHandlerMethod ihm = new InvocableHandlerMethod(bean, method);
|
||||
for (MethodParameter mp : ihm.getMethodParameters()) {
|
||||
|
||||
ShellOption so = mp.getParameterAnnotation(ShellOption.class);
|
||||
log.debug("Registering with mp='{}' so='{}'", mp, so);
|
||||
if (so != null) {
|
||||
List<String> longNames = new ArrayList<>();
|
||||
List<Character> shortNames = new ArrayList<>();
|
||||
if (!ObjectUtils.isEmpty(so.value())) {
|
||||
Arrays.asList(so.value()).stream().forEach(o -> {
|
||||
String stripped = StringUtils.trimLeadingCharacter(o, '-');
|
||||
log.debug("Registering o='{}' stripped='{}'", o, stripped);
|
||||
if (o.length() == stripped.length() + 2) {
|
||||
longNames.add(stripped);
|
||||
}
|
||||
else if (o.length() == stripped.length() + 1 && stripped.length() == 1) {
|
||||
shortNames.add(stripped.charAt(0));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
// ShellOption value not defined
|
||||
mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
|
||||
String longName = mp.getParameterName();
|
||||
Class<?> parameterType = mp.getParameterType();
|
||||
if (longName != null) {
|
||||
log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType);
|
||||
longNames.add(longName);
|
||||
}
|
||||
}
|
||||
if (!longNames.isEmpty() || !shortNames.isEmpty()) {
|
||||
log.debug("Registering longNames='{}' shortNames='{}'", longNames, shortNames);
|
||||
OptionSpec optionSpec = builder.withOption()
|
||||
.type(mp.getParameterType())
|
||||
.longNames(longNames.toArray(new String[0]))
|
||||
.shortNames(shortNames.toArray(new Character[0]))
|
||||
.position(mp.getParameterIndex())
|
||||
.description(so.help());
|
||||
if (so.arity() > -1) {
|
||||
optionSpec.arity(0, so.arity());
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
mp.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
|
||||
String longName = mp.getParameterName();
|
||||
Class<?> parameterType = mp.getParameterType();
|
||||
if (longName != null) {
|
||||
log.debug("Using mp='{}' longName='{}' parameterType='{}'", mp, longName, parameterType);
|
||||
builder.withOption()
|
||||
.longNames(longName)
|
||||
.type(parameterType)
|
||||
.required()
|
||||
.position(mp.getParameterIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.withTarget().method(bean, method);
|
||||
|
||||
CommandRegistration registration = builder.build();
|
||||
registry.register(registration);
|
||||
}
|
||||
}, method -> method.getAnnotation(ShellMethod.class) != null);
|
||||
}
|
||||
@@ -190,10 +263,4 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + " contributing "
|
||||
+ collectionToDelimitedString(commands.keySet(), ", ", "[", "]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,573 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-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.standard;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Executable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.ValidatorFactory;
|
||||
import javax.validation.metadata.MethodDescriptor;
|
||||
import javax.validation.metadata.ParameterDescriptor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.ParameterMissingResolutionException;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.UnfinishedParameterResolutionException;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.ValueResult;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import static org.springframework.shell.Utils.unCamelify;
|
||||
|
||||
/**
|
||||
* Default ParameterResolver implementation that supports the following features:
|
||||
* <ul>
|
||||
* <li>named parameters (recognized because they start with some
|
||||
* {@link ShellMethod#prefix()})</li>
|
||||
* <li>implicit named parameters (from the actual method parameter name)</li>
|
||||
* <li>positional parameters (in order, for all parameter values that were not resolved
|
||||
* <i>via</i> named parameters)</li>
|
||||
* <li>default values (for all remaining parameters)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Method arguments can consume several words of input at once (driven by
|
||||
* {@link ShellOption#arity()}, default 1). If several words are consumed, they will be
|
||||
* joined together as a comma separated value and passed to the {@link ConversionService}
|
||||
* (which will typically return a List or array).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Boolean parameters are by default expected to have an arity of 0, allowing invocations
|
||||
* in the form {@code rm
|
||||
* --force --dir /foo}: the presence of {@code --force} passes {@code true} as a parameter
|
||||
* value, while its absence passes {@code false}. Both the default arity of 0 and the
|
||||
* default value of {@code false} can be overridden <i>via</i> {@link ShellOption} if
|
||||
* needed.
|
||||
* </p>
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
* @author Camilo Gonzalez
|
||||
*/
|
||||
@Component
|
||||
public class StandardParameterResolver implements ParameterResolver {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private Collection<ValueProvider> valueProviders = new HashSet<>();
|
||||
|
||||
private Validator validator = Utils.defaultValidator();
|
||||
|
||||
/**
|
||||
* A cache from method+input to String representation of actual parameter values. Note
|
||||
* that the converted result is not cached, to allow dynamic computation to happen at
|
||||
* every invocation if needed (e.g. if a remote service is involved).
|
||||
*/
|
||||
private final Map<CacheKey, Map<Parameter, ParameterRawValue>> parameterCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
public StandardParameterResolver(ConversionService conversionService, Set<ValueProvider> valueProviders) {
|
||||
this.conversionService = conversionService;
|
||||
this.valueProviders = valueProviders;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setValidatorFactory(ValidatorFactory validatorFactory) {
|
||||
this.validator = validatorFactory.getValidator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter parameter) {
|
||||
boolean optOut = parameter.hasParameterAnnotation(ShellOption.class)
|
||||
&& parameter.getParameterAnnotation(ShellOption.class).optOut();
|
||||
return !optOut && parameter.getMethodAnnotation(ShellMethod.class) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueResult resolve(MethodParameter methodParameter, List<String> wordsBuffer) {
|
||||
List<String> words = wordsBuffer.stream().filter(w -> !w.isEmpty()).collect(Collectors.toList());
|
||||
|
||||
CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), wordsBuffer);
|
||||
parameterCache.clear();
|
||||
Map<Parameter, ParameterRawValue> resolved = parameterCache.computeIfAbsent(cacheKey, (k) -> {
|
||||
|
||||
Map<Parameter, ParameterRawValue> result = new HashMap<>();
|
||||
Map<String, String> namedParameters = new HashMap<>();
|
||||
|
||||
// index of words that haven't yet been used to resolve parameter values
|
||||
List<Integer> unusedWords = new ArrayList<>();
|
||||
|
||||
Set<String> possibleKeys = gatherAllPossibleKeys(methodParameter.getMethod());
|
||||
|
||||
// First, resolve all parameters passed by-name
|
||||
for (int i = 0; i < words.size(); i++) {
|
||||
int from = i;
|
||||
String word = words.get(i);
|
||||
if (possibleKeys.contains(word)) {
|
||||
String key = word;
|
||||
Parameter parameter = lookupParameterForKey(methodParameter.getMethod(), key);
|
||||
int arity = getArity(parameter);
|
||||
|
||||
if (i + 1 + arity > words.size()) {
|
||||
String input = words.subList(i, words.size()).stream().collect(Collectors.joining(" "));
|
||||
throw new UnfinishedParameterResolutionException(
|
||||
describe(Utils.createMethodParameter(parameter)).findFirst().get(), input);
|
||||
}
|
||||
Assert.isTrue(i + 1 + arity <= words.size(),
|
||||
String.format("Not enough input for parameter '%s'", word));
|
||||
String raw = words.subList(i + 1, i + 1 + arity).stream().collect(Collectors.joining(","));
|
||||
Assert.isTrue(!namedParameters.containsKey(key),
|
||||
String.format("Parameter for '%s' has already been specified", word));
|
||||
namedParameters.put(key, raw);
|
||||
if (arity == 0) {
|
||||
boolean defaultValue = booleanDefaultValue(parameter);
|
||||
// Boolean parameter has been specified. Use the opposite of the default value
|
||||
result.put(parameter,
|
||||
ParameterRawValue.explicit(String.valueOf(!defaultValue), key, from, from));
|
||||
}
|
||||
else {
|
||||
i += arity;
|
||||
result.put(parameter, ParameterRawValue.explicit(raw, key, from, i));
|
||||
}
|
||||
} // store for later processing of positional params
|
||||
else {
|
||||
unusedWords.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Now have a second pass over params and treat them as positional
|
||||
int offset = 0;
|
||||
Parameter[] parameters = methodParameter.getMethod().getParameters();
|
||||
for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) {
|
||||
Parameter parameter = parameters[i];
|
||||
// Compute the intersection between possible keys for the param and what we've already
|
||||
// seen for named params
|
||||
Collection<String> keys = getKeysForParameter(methodParameter.getMethod(), i)
|
||||
.collect(Collectors.toSet());
|
||||
Collection<String> copy = new HashSet<>(keys);
|
||||
copy.retainAll(namedParameters.keySet());
|
||||
if (copy.isEmpty()) { // Was not set via a key (including aliases), must be positional
|
||||
int arity = getArity(parameter);
|
||||
if (arity > 0 && (offset + arity) <= unusedWords.size()) {
|
||||
String raw = unusedWords.subList(offset, offset + arity).stream()
|
||||
.map(index -> words.get(index))
|
||||
.collect(Collectors.joining(","));
|
||||
int from = unusedWords.get(offset);
|
||||
int to = from + arity - 1;
|
||||
result.put(parameter, ParameterRawValue.explicit(raw, null, from, to));
|
||||
offset += arity;
|
||||
} // No more input. Try defaultValues
|
||||
else {
|
||||
Optional<String> defaultValue = defaultValueFor(parameter);
|
||||
defaultValue.ifPresent(
|
||||
value -> result.put(parameter, ParameterRawValue.implicit(value, null, null, null)));
|
||||
}
|
||||
}
|
||||
else if (copy.size() > 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Named parameter has been specified multiple times via " + quote(copy));
|
||||
}
|
||||
}
|
||||
|
||||
Assert.isTrue(offset == unusedWords.size(),
|
||||
"Too many arguments: the following could not be mapped to parameters: "
|
||||
+ unusedWords.subList(offset, unusedWords.size()).stream()
|
||||
.map(index -> words.get(index)).collect(Collectors.joining(" ", "'", "'")));
|
||||
return result;
|
||||
});
|
||||
|
||||
Parameter param = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()];
|
||||
if (!resolved.containsKey(param)) {
|
||||
throw new ParameterMissingResolutionException(describe(methodParameter).findFirst().get());
|
||||
}
|
||||
ParameterRawValue parameterRawValue = resolved.get(param);
|
||||
Object value = convertRawValue(parameterRawValue, methodParameter);
|
||||
BitSet wordsUsed = getWordsUsed(parameterRawValue);
|
||||
BitSet wordsUsedForValue = getWordsUsedForValue(parameterRawValue);
|
||||
return new ValueResult(methodParameter, value, wordsUsed, wordsUsedForValue);
|
||||
}
|
||||
|
||||
private BitSet getWordsUsed(ParameterRawValue parameterRawValue) {
|
||||
if (parameterRawValue.from != null) {
|
||||
BitSet wordsUsed = new BitSet();
|
||||
wordsUsed.set(parameterRawValue.from, parameterRawValue.to + 1);
|
||||
return wordsUsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private BitSet getWordsUsedForValue(ParameterRawValue parameterRawValue) {
|
||||
if (parameterRawValue.from != null) {
|
||||
BitSet wordsUsedForValue = new BitSet();
|
||||
wordsUsedForValue.set(parameterRawValue.from, parameterRawValue.to + 1);
|
||||
if (parameterRawValue.key != null) {
|
||||
wordsUsedForValue.clear(parameterRawValue.from);
|
||||
}
|
||||
return wordsUsedForValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object convertRawValue(ParameterRawValue parameterRawValue, MethodParameter methodParameter) {
|
||||
String s = parameterRawValue.value;
|
||||
if (ShellOption.NULL.equals(s)) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return conversionService.convert(s, TypeDescriptor.valueOf(String.class),
|
||||
new TypeDescriptor(methodParameter));
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> gatherAllPossibleKeys(Method method) {
|
||||
return Arrays.stream(method.getParameters())
|
||||
.flatMap(this::getKeysForParameter)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private String prefixForMethod(Executable method) {
|
||||
return method.getAnnotation(ShellMethod.class).prefix();
|
||||
}
|
||||
|
||||
private Optional<String> defaultValueFor(Parameter parameter) {
|
||||
ShellOption option = parameter.getAnnotation(ShellOption.class);
|
||||
if (option != null && !ShellOption.NONE.equals(option.defaultValue())) {
|
||||
return Optional.of(option.defaultValue());
|
||||
}
|
||||
else if (getArity(parameter) == 0) {
|
||||
return Optional.of("false");
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private boolean booleanDefaultValue(Parameter parameter) {
|
||||
ShellOption option = parameter.getAnnotation(ShellOption.class);
|
||||
if (option != null && !ShellOption.NONE.equals(option.defaultValue())) {
|
||||
return Boolean.parseBoolean(option.defaultValue());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ParameterDescription> describe(MethodParameter parameter) {
|
||||
Parameter jlrParameter = parameter.getMethod().getParameters()[parameter.getParameterIndex()];
|
||||
int arity = getArity(jlrParameter);
|
||||
Class<?> type = parameter.getParameterType();
|
||||
ShellOption option = jlrParameter.getAnnotation(ShellOption.class);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < arity; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(" ");
|
||||
}
|
||||
sb.append(arity > 1 ? unCamelify(removeMultiplicityFromType(parameter).getSimpleName())
|
||||
: unCamelify(type.getSimpleName()));
|
||||
}
|
||||
ParameterDescription result = ParameterDescription.outOf(parameter);
|
||||
result.formal(sb.toString());
|
||||
if (option != null) {
|
||||
result.help(option.help());
|
||||
Optional<String> defaultValue = defaultValueFor(jlrParameter);
|
||||
if (defaultValue.isPresent()) {
|
||||
result.defaultValue(defaultValue.map(dv -> dv.equals(ShellOption.NULL) ? "<none>" : dv).get());
|
||||
}
|
||||
}
|
||||
result
|
||||
.keys(getKeysForParameter(parameter.getMethod(), parameter.getParameterIndex())
|
||||
.collect(Collectors.toList()))
|
||||
.mandatoryKey(false);
|
||||
|
||||
MethodDescriptor constraintsForMethod = validator.getConstraintsForClass(parameter.getDeclaringClass())
|
||||
.getConstraintsForMethod(parameter.getMethod().getName(), parameter.getMethod().getParameterTypes());
|
||||
if (constraintsForMethod != null) {
|
||||
ParameterDescriptor constraintsDescriptor = constraintsForMethod
|
||||
.getParameterDescriptors().get(parameter.getParameterIndex());
|
||||
result.elementDescriptor(constraintsDescriptor);
|
||||
}
|
||||
|
||||
return Stream.of(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompletionProposal> complete(MethodParameter methodParameter, CompletionContext context) {
|
||||
boolean set;
|
||||
Exception unfinished = null;
|
||||
// First try to see if this parameter has been set, even to some unfinished value
|
||||
ParameterRawValue parameterRawValue = null;
|
||||
int arity = 1;
|
||||
try {
|
||||
resolve(methodParameter, context.getWords());
|
||||
CacheKey cacheKey = new CacheKey(methodParameter.getMethod(), context.getWords());
|
||||
Parameter parameter = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()];
|
||||
arity = getArity(parameter);
|
||||
parameterRawValue = parameterCache.get(cacheKey).get(parameter);
|
||||
set = parameterRawValue.explicit;
|
||||
}
|
||||
catch (ParameterMissingResolutionException e) {
|
||||
set = false;
|
||||
}
|
||||
catch (UnfinishedParameterResolutionException e) {
|
||||
if (e.getParameterDescription().parameter().equals(methodParameter)) {
|
||||
unfinished = e;
|
||||
set = false;
|
||||
}
|
||||
else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Most likely what is already typed would fail resolution (eg type conversion failure)
|
||||
return argumentKeysThatStartWithContextPrefix(methodParameter, context);
|
||||
}
|
||||
|
||||
// There are 4 possible cases:
|
||||
// 1) parameter not set at all
|
||||
// 2) parameter set via its key, not enough input to consume a value
|
||||
// 3) parameter set with multiple values, enough to cover arity. We're done
|
||||
// 4) parameter set, and some value bound. But maybe that value is just a prefix to what
|
||||
// the user actually wants
|
||||
// 4.1) or maybe that value was resolved by position, but is a prefix of an actual valid
|
||||
// key
|
||||
|
||||
if (!set) {
|
||||
if (unfinished == null) {
|
||||
// case 1 above
|
||||
return argumentKeysThatStartWithContextPrefix(methodParameter, context);
|
||||
} // case 2
|
||||
else {
|
||||
return valueCompletions(methodParameter, context);
|
||||
}
|
||||
}
|
||||
else {
|
||||
List<CompletionProposal> result = new ArrayList<>();
|
||||
|
||||
Object value = convertRawValue(parameterRawValue, methodParameter);
|
||||
if (value instanceof Collection && ((Collection<?>) value).size() == arity
|
||||
|| (ObjectUtils.isArray(value) && Array.getLength(value) == arity)) {
|
||||
// We're done already
|
||||
return result;
|
||||
}
|
||||
if (!context.currentWord().equals("")) {
|
||||
// Case 4
|
||||
result.addAll(valueCompletions(methodParameter, context));
|
||||
}
|
||||
|
||||
if (parameterRawValue.positional()) {
|
||||
// Case 4.1: There exists "--command foo" and user has typed "--comm" which (wrongly) got
|
||||
// resolved as a positional param
|
||||
result.addAll(argumentKeysThatStartWithContextPrefix(methodParameter, context));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private List<CompletionProposal> valueCompletions(MethodParameter methodParameter,
|
||||
CompletionContext completionContext) {
|
||||
return valueProviders.stream()
|
||||
.filter(vp -> vp.supports(methodParameter, completionContext))
|
||||
.map(vp -> vp.complete(methodParameter, completionContext, null))
|
||||
.findFirst().orElseGet(() -> Collections.emptyList());
|
||||
}
|
||||
|
||||
private List<CompletionProposal> argumentKeysThatStartWithContextPrefix(MethodParameter methodParameter,
|
||||
CompletionContext context) {
|
||||
String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : "";
|
||||
return describe(methodParameter).flatMap(pd -> pd.keys().stream())
|
||||
.filter(k -> k.startsWith(prefix))
|
||||
.map(CompletionProposal::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of {@code foo[] or Collection<Foo>} and arity > 1, return the element type.
|
||||
*/
|
||||
private Class<?> removeMultiplicityFromType(MethodParameter parameter) {
|
||||
Class<?> parameterType = parameter.getParameterType();
|
||||
if (parameterType.isArray()) {
|
||||
return parameterType.getComponentType();
|
||||
}
|
||||
else if (Collection.class.isAssignableFrom(parameterType)) {
|
||||
return parameter.getNestedParameterType();
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("For " + parameter + " (with arity > 1) expected an array/collection type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Surrounds the parameter keys with quotes.
|
||||
*/
|
||||
private String quote(Collection<String> keys) {
|
||||
return keys.stream().collect(Collectors.joining(", ", "'", "'"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the arity of a given parameter. The default arity is 1, except for booleans
|
||||
* where arity is 0 (can be overridden back to 1 via an annotation)
|
||||
*/
|
||||
private int getArity(Parameter parameter) {
|
||||
ShellOption option = parameter.getAnnotation(ShellOption.class);
|
||||
int inferred = (parameter.getType() == boolean.class || parameter.getType() == Boolean.class) ? 0 : 1;
|
||||
return option != null && option.arity() != ShellOption.ARITY_USE_HEURISTICS ? option.arity() : inferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key(s) for the i-th parameter of the command method, resolved either from
|
||||
* the {@link ShellOption} annotation, or from the actual parameter name.
|
||||
*/
|
||||
private Stream<String> getKeysForParameter(Method method, int index) {
|
||||
Parameter p = method.getParameters()[index];
|
||||
return getKeysForParameter(p);
|
||||
}
|
||||
|
||||
private Stream<String> getKeysForParameter(Parameter p) {
|
||||
Executable method = p.getDeclaringExecutable();
|
||||
String prefix = prefixForMethod(method);
|
||||
ShellOption option = p.getAnnotation(ShellOption.class);
|
||||
if (option != null && option.value().length > 0) {
|
||||
return Arrays.stream(option.value());
|
||||
}
|
||||
else {
|
||||
return Stream.of(prefix + Utils.unCamelify(Utils.createMethodParameter(p).getParameterName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method parameter that should be bound to the given key.
|
||||
*/
|
||||
private Parameter lookupParameterForKey(Method method, String key) {
|
||||
Parameter[] parameters = method.getParameters();
|
||||
for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) {
|
||||
Parameter p = parameters[i];
|
||||
if (getKeysForParameter(method, i).anyMatch(k -> k.equals(key))) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(String.format("Could not look up parameter for '%s' in %s", key, method));
|
||||
}
|
||||
|
||||
private static class CacheKey {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final List<String> words;
|
||||
|
||||
private CacheKey(Method method, List<String> words) {
|
||||
this.method = method;
|
||||
this.words = words;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
CacheKey cacheKey = (CacheKey) o;
|
||||
return Objects.equals(method, cacheKey.method) &&
|
||||
Objects.equals(words, cacheKey.words);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(method, words);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return method.getName() + " " + words;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParameterRawValue {
|
||||
|
||||
private Integer from;
|
||||
|
||||
private Integer to;
|
||||
|
||||
/**
|
||||
* The raw String value that got bound to a parameter.
|
||||
*/
|
||||
private final String value;
|
||||
|
||||
/**
|
||||
* If false, the value resolved is the result of applying defaults.
|
||||
*/
|
||||
private final boolean explicit;
|
||||
|
||||
/**
|
||||
* The key that was used to set the parameter, or null if resolution happened by position.
|
||||
*/
|
||||
private final String key;
|
||||
|
||||
private ParameterRawValue(String value, boolean explicit, String key, Integer from, Integer to) {
|
||||
this.value = value;
|
||||
this.explicit = explicit;
|
||||
this.key = key;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
public static ParameterRawValue explicit(String value, String key, Integer from, Integer to) {
|
||||
return new ParameterRawValue(value, true, key, from, to);
|
||||
}
|
||||
|
||||
public static ParameterRawValue implicit(String value, String key, Integer from, Integer to) {
|
||||
return new ParameterRawValue(value, false, key, from, to);
|
||||
}
|
||||
|
||||
public boolean positional() {
|
||||
return key == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ParameterRawValue{" +
|
||||
"value='" + value + '\'' +
|
||||
", explicit=" + explicit +
|
||||
", key='" + key + '\'' +
|
||||
", from=" + from +
|
||||
", to=" + to +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,10 +22,10 @@ import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@@ -36,11 +36,8 @@ import org.stringtemplate.v4.STGroupString;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterDescription;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -54,14 +51,11 @@ import org.springframework.util.MultiValueMap;
|
||||
public abstract class AbstractCompletions {
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final CommandRegistry commandRegistry;
|
||||
private final List<ParameterResolver> parameterResolvers;
|
||||
private final CommandCatalog commandCatalog;
|
||||
|
||||
public AbstractCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
public AbstractCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.commandRegistry = commandRegistry;
|
||||
this.parameterResolvers = parameterResolvers;
|
||||
this.commandCatalog = commandCatalog;
|
||||
}
|
||||
|
||||
protected Builder builder() {
|
||||
@@ -74,12 +68,12 @@ public abstract class AbstractCompletions {
|
||||
* all needed to build completions structure.
|
||||
*/
|
||||
protected CommandModel generateCommandModel() {
|
||||
Map<String, MethodTarget> commandsByName = commandRegistry.listCommands();
|
||||
Collection<CommandRegistration> commandsByName = commandCatalog.getRegistrations().values();
|
||||
HashMap<String, DefaultCommandModelCommand> commands = new HashMap<>();
|
||||
HashSet<CommandModelCommand> topCommands = new HashSet<>();
|
||||
commandsByName.entrySet().stream()
|
||||
.forEach(entry -> {
|
||||
String key = entry.getKey();
|
||||
commandsByName.stream()
|
||||
.forEach(registration -> {
|
||||
String key = registration.getCommand();
|
||||
String[] splitKeys = key.split(" ");
|
||||
String commandKey = "";
|
||||
for (int i = 0; i < splitKeys.length; i++) {
|
||||
@@ -94,12 +88,13 @@ public abstract class AbstractCompletions {
|
||||
}
|
||||
DefaultCommandModelCommand command = commands.computeIfAbsent(commandKey,
|
||||
(fullCommand) -> new DefaultCommandModelCommand(fullCommand, main));
|
||||
MethodTarget methodTarget = entry.getValue();
|
||||
List<ParameterDescription> parameterDescriptions = getParameterDescriptions(methodTarget);
|
||||
List<DefaultCommandModelOption> options = parameterDescriptions.stream()
|
||||
.flatMap(pd -> pd.keys().stream())
|
||||
.map(k -> new DefaultCommandModelOption(k))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// TODO long vs short
|
||||
List<CommandModelOption> options = registration.getOptions().stream()
|
||||
.flatMap(co -> Arrays.stream(co.getLongNames()))
|
||||
.map(lo -> CommandModelOption.of("--", lo))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (i == splitKeys.length - 1) {
|
||||
command.addOptions(options);
|
||||
}
|
||||
@@ -114,13 +109,6 @@ public abstract class AbstractCompletions {
|
||||
return new DefaultCommandModel(new ArrayList<>(topCommands));
|
||||
}
|
||||
|
||||
private List<ParameterDescription> getParameterDescriptions(MethodTarget methodTarget) {
|
||||
return Utils.createMethodParameters(methodTarget.getMethod())
|
||||
.flatMap(mp -> parameterResolvers.stream().filter(pr -> pr.supports(mp)).limit(1L)
|
||||
.flatMap(pr -> pr.describe(mp)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a command model structure. Is also used as entry model
|
||||
* for ST4 templates which is a reason it has utility methods for easier usage
|
||||
@@ -208,6 +196,10 @@ public abstract class AbstractCompletions {
|
||||
|
||||
interface CommandModelOption {
|
||||
String option();
|
||||
|
||||
static CommandModelOption of(String prefix, String name) {
|
||||
return new DefaultCommandModelOption(String.format("%s%s", prefix, name));
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultCommandModel implements CommandModel {
|
||||
@@ -294,7 +286,7 @@ public abstract class AbstractCompletions {
|
||||
return options;
|
||||
}
|
||||
|
||||
void addOptions(List<DefaultCommandModelOption> options) {
|
||||
void addOptions(List<CommandModelOption> options) {
|
||||
this.options.addAll(options);
|
||||
}
|
||||
|
||||
@@ -341,7 +333,7 @@ public abstract class AbstractCompletions {
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultCommandModelOption implements CommandModelOption {
|
||||
static class DefaultCommandModelOption implements CommandModelOption {
|
||||
|
||||
private String option;
|
||||
|
||||
|
||||
@@ -15,11 +15,8 @@
|
||||
*/
|
||||
package org.springframework.shell.standard.completion;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
|
||||
/**
|
||||
* Completion script generator for a {@code bash}.
|
||||
@@ -28,9 +25,8 @@ import org.springframework.shell.ParameterResolver;
|
||||
*/
|
||||
public class BashCompletions extends AbstractCompletions {
|
||||
|
||||
public BashCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
super(resourceLoader, commandRegistry, parameterResolvers);
|
||||
public BashCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
|
||||
super(resourceLoader, commandCatalog);
|
||||
}
|
||||
|
||||
public String generate(String rootCommand) {
|
||||
|
||||
@@ -29,11 +29,11 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -47,7 +47,7 @@ import static org.mockito.Mockito.when;
|
||||
public class CommandValueProviderTest {
|
||||
|
||||
@Mock
|
||||
private CommandRegistry shell;
|
||||
private CommandCatalog catalog;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
@@ -56,7 +56,7 @@ public class CommandValueProviderTest {
|
||||
|
||||
@Test
|
||||
public void testValues() {
|
||||
CommandValueProvider valueProvider = new CommandValueProvider(shell);
|
||||
CommandValueProvider valueProvider = new CommandValueProvider(catalog);
|
||||
|
||||
Method help = ReflectionUtils.findMethod(Command.class, "help", String.class);
|
||||
MethodParameter methodParameter = Utils.createMethodParameter(help, 0);
|
||||
@@ -64,12 +64,12 @@ public class CommandValueProviderTest {
|
||||
boolean supports = valueProvider.supports(methodParameter, completionContext);
|
||||
|
||||
assertThat(supports).isEqualTo(true);
|
||||
Map<String, CommandRegistration> registrations = new HashMap<>();
|
||||
registrations.put("me", null);
|
||||
registrations.put("meow", null);
|
||||
registrations.put("yourself", null);
|
||||
|
||||
Map<String, MethodTarget> commands = new HashMap<>();
|
||||
commands.put("me", null);
|
||||
commands.put("meow", null);
|
||||
commands.put("yourself", null);
|
||||
when(shell.listCommands()).thenReturn(commands);
|
||||
when(catalog.getRegistrations()).thenReturn(registrations);
|
||||
List<CompletionProposal> proposals = valueProvider.complete(methodParameter, completionContext, new String[0]);
|
||||
|
||||
assertThat(proposals).extracting("value", String.class)
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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.standard;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.standard.test1.GroupOneCommands;
|
||||
import org.springframework.shell.standard.test2.GroupThreeCommands;
|
||||
import org.springframework.shell.standard.test2.GroupTwoCommands;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StandardMethodTargetRegistrar}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class StandardMethodTargetRegistrarTest {
|
||||
|
||||
private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
|
||||
private ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
|
||||
@Test
|
||||
public void testRegistrations() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Sample.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
|
||||
MethodTarget methodTarget = registry.listCommands().get("say-hello");
|
||||
assertThat(methodTarget).isNotNull();
|
||||
assertThat(methodTarget.getHelp()).isEqualTo("some command");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "sayHello", String.class));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
|
||||
methodTarget = registry.listCommands().get("hi");
|
||||
assertThat(methodTarget).isNotNull();
|
||||
assertThat(methodTarget.getHelp()).isEqualTo("method with alias");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "greet", String.class));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
methodTarget = registry.listCommands().get("alias");
|
||||
assertThat(methodTarget).isNotNull();
|
||||
assertThat(methodTarget.getHelp()).isEqualTo("method with alias");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(Sample.class, "greet", String.class));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class Sample {
|
||||
|
||||
@ShellMethod("some command")
|
||||
public String sayHello(String what) {
|
||||
return "hello " + what;
|
||||
}
|
||||
|
||||
@ShellMethod(value = "method with alias", key = {"hi", "alias"})
|
||||
public String greet(String what) {
|
||||
return "hi " + what;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicators() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class);
|
||||
|
||||
MethodTarget methodTarget = registry.listCommands().get("say-hello");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "sayHello"));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
|
||||
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("sayHelloAvailability");
|
||||
sample.available = true;
|
||||
|
||||
methodTarget = registry.listCommands().get("hi");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "hi"));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
|
||||
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("customAvailabilityMethod");
|
||||
sample.available = true;
|
||||
|
||||
methodTarget = registry.listCommands().get("bonjour");
|
||||
assertThat(methodTarget.getMethod()).isEqualTo(ReflectionUtils.findMethod(SampleWithAvailability.class, "bonjour"));
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(methodTarget.getAvailability().isAvailable()).isFalse();
|
||||
assertThat(methodTarget.getAvailability().getReason()).isEqualTo("availabilityForSeveralCommands");
|
||||
sample.available = true;
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class SampleWithAvailability {
|
||||
|
||||
private boolean available = true;
|
||||
|
||||
@ShellMethod("some command with an implicit availability indicator")
|
||||
public void sayHello() {
|
||||
|
||||
}
|
||||
public Availability sayHelloAvailability() {
|
||||
return available ? Availability.available() : Availability.unavailable("sayHelloAvailability");
|
||||
}
|
||||
|
||||
|
||||
@ShellMethodAvailability("customAvailabilityMethod")
|
||||
@ShellMethod("some method with an explicit availability indicator")
|
||||
public void hi() {
|
||||
|
||||
}
|
||||
public Availability customAvailabilityMethod() {
|
||||
return available ? Availability.available() : Availability.unavailable("customAvailabilityMethod");
|
||||
}
|
||||
|
||||
@ShellMethod(value = "some method with an explicit availability indicator", key = {"bonjour", "salut"})
|
||||
public void bonjour() {
|
||||
|
||||
}
|
||||
@ShellMethodAvailability({"salut", "other"})
|
||||
public Availability availabilityForSeveralCommands() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityForSeveralCommands");
|
||||
}
|
||||
|
||||
|
||||
@ShellMethod("a command whose availability indicator will come from wildcard")
|
||||
public void wild() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethodAvailability("*")
|
||||
private Availability availabilityFromWildcard() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityFromWildcard");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorErrorMultipleExplicit() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(registry);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("When set on a @ShellMethod method, the value of the @ShellMethodAvailability should be a single element")
|
||||
.hasMessageContaining("Found [one, two]")
|
||||
.hasMessageContaining("wrong()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorOnShellMethod {
|
||||
|
||||
@ShellMethodAvailability({"one", "two"})
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorWildcardNotAlone() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(registry);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("When using '*' as a wildcard for ShellMethodAvailability, this can be the only value. Found [one, *]")
|
||||
.hasMessageContaining("availability()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorWildcardNotAlone {
|
||||
|
||||
@ShellMethodAvailability({"one", "*"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorAmbiguous() {
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(registry);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Found several @ShellMethodAvailability")
|
||||
.hasMessageContaining("wrong()")
|
||||
.hasMessageContaining("availability()")
|
||||
.hasMessageContaining("otherAvailability()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorAmbiguous {
|
||||
|
||||
@ShellMethodAvailability({"one", "wrong"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethodAvailability({"bar", "wrong"})
|
||||
public Availability otherAvailability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrouping() {
|
||||
ApplicationContext context = new AnnotationConfigApplicationContext(GroupOneCommands.class,
|
||||
GroupTwoCommands.class, GroupThreeCommands.class);
|
||||
registrar.setApplicationContext(context);
|
||||
registrar.register(registry);
|
||||
|
||||
Map<String, MethodTarget> commands = registry.listCommands();
|
||||
Assertions.assertThat(commands.get("explicit1").getGroup()).isEqualTo("Explicit Group Method Level 1");
|
||||
Assertions.assertThat(commands.get("explicit2").getGroup()).isEqualTo("Explicit Group Method Level 2");
|
||||
Assertions.assertThat(commands.get("explicit3").getGroup()).isEqualTo("Explicit Group Method Level 3");
|
||||
Assertions.assertThat(commands.get("implicit1").getGroup()).isEqualTo("Implicit Group Package Level 1");
|
||||
Assertions.assertThat(commands.get("implicit2").getGroup()).isEqualTo("Group Two Commands");
|
||||
Assertions.assertThat(commands.get("implicit3").getGroup()).isEqualTo("Explicit Group 3 Class Level");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteractionModeInteractive() {
|
||||
DefaultShellContext shellContext = new DefaultShellContext();
|
||||
shellContext.setInteractionMode(InteractionMode.INTERACTIVE);
|
||||
registry = new ConfigurableCommandRegistry(shellContext);
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
|
||||
assertThat(registry.listCommands().get("foo1")).isNotNull();
|
||||
assertThat(registry.listCommands().get("foo2")).isNull();
|
||||
assertThat(registry.listCommands().get("foo3")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteractionModeNonInteractive() {
|
||||
DefaultShellContext shellContext = new DefaultShellContext();
|
||||
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
|
||||
registry = new ConfigurableCommandRegistry(shellContext);
|
||||
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(registry);
|
||||
|
||||
assertThat(registry.listCommands().get("foo1")).isNull();
|
||||
assertThat(registry.listCommands().get("foo2")).isNotNull();
|
||||
assertThat(registry.listCommands().get("foo3")).isNotNull();
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class InteractionModeCommands {
|
||||
|
||||
@ShellMethod(value = "foo1", interactionMode = InteractionMode.INTERACTIVE)
|
||||
public void foo1() {
|
||||
}
|
||||
|
||||
@ShellMethod(value = "foo2", interactionMode = InteractionMode.NONINTERACTIVE)
|
||||
public void foo2() {
|
||||
}
|
||||
|
||||
@ShellMethod(value = "foo3")
|
||||
public void foo3() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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.standard;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
import org.springframework.shell.context.InteractionMode;
|
||||
import org.springframework.shell.standard.test1.GroupOneCommands;
|
||||
import org.springframework.shell.standard.test2.GroupThreeCommands;
|
||||
import org.springframework.shell.standard.test2.GroupTwoCommands;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StandardMethodTargetRegistrar}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class StandardMethodTargetRegistrarTests {
|
||||
|
||||
private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar();
|
||||
private AnnotationConfigApplicationContext applicationContext;
|
||||
private CommandCatalog catalog;
|
||||
private DefaultShellContext shellContext;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
shellContext = new DefaultShellContext();
|
||||
catalog = CommandCatalog.of(null, shellContext);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
if (applicationContext != null) {
|
||||
applicationContext.close();
|
||||
}
|
||||
applicationContext = null;
|
||||
catalog = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrations() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(Sample.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
|
||||
assertThat(registrations).hasSize(3);
|
||||
|
||||
assertThat(registrations.get("say-hello")).isNotNull();
|
||||
assertThat(registrations.get("say-hello").getAvailability()).isNotNull();
|
||||
assertThat(registrations.get("say-hello").getOptions()).hasSize(1);
|
||||
assertThat(registrations.get("say-hello").getOptions().get(0).getLongNames()).containsExactly("what");
|
||||
|
||||
assertThat(registrations.get("hi")).isNotNull();
|
||||
assertThat(registrations.get("hi").getAvailability()).isNotNull();
|
||||
|
||||
assertThat(registrations.get("alias")).isNotNull();
|
||||
assertThat(registrations.get("alias").getAvailability()).isNotNull();
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class Sample {
|
||||
|
||||
@ShellMethod("some command")
|
||||
public String sayHello(String what) {
|
||||
return "hello " + what;
|
||||
}
|
||||
|
||||
@ShellMethod(value = "method with alias", key = {"hi", "alias"})
|
||||
public String greet(String what) {
|
||||
return "hi " + what;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicators() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class);
|
||||
SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
Map<String, CommandRegistration> registrations = catalog.getRegistrations();
|
||||
|
||||
assertThat(registrations.get("say-hello")).isNotNull();
|
||||
assertThat(registrations.get("say-hello").getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(registrations.get("say-hello").getAvailability().isAvailable()).isFalse();
|
||||
assertThat(registrations.get("say-hello").getAvailability().getReason()).isEqualTo("sayHelloAvailability");
|
||||
sample.available = true;
|
||||
|
||||
assertThat(registrations.get("hi")).isNotNull();
|
||||
assertThat(registrations.get("hi").getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(registrations.get("hi").getAvailability().isAvailable()).isFalse();
|
||||
assertThat(registrations.get("hi").getAvailability().getReason()).isEqualTo("customAvailabilityMethod");
|
||||
sample.available = true;
|
||||
|
||||
assertThat(registrations.get("bonjour")).isNotNull();
|
||||
assertThat(registrations.get("bonjour").getAvailability().isAvailable()).isTrue();
|
||||
sample.available = false;
|
||||
assertThat(registrations.get("bonjour").getAvailability().isAvailable()).isFalse();
|
||||
assertThat(registrations.get("bonjour").getAvailability().getReason()).isEqualTo("availabilityForSeveralCommands");
|
||||
sample.available = true;
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class SampleWithAvailability {
|
||||
|
||||
private boolean available = true;
|
||||
|
||||
@ShellMethod("some command with an implicit availability indicator")
|
||||
public void sayHello() {
|
||||
|
||||
}
|
||||
public Availability sayHelloAvailability() {
|
||||
return available ? Availability.available() : Availability.unavailable("sayHelloAvailability");
|
||||
}
|
||||
|
||||
|
||||
@ShellMethodAvailability("customAvailabilityMethod")
|
||||
@ShellMethod("some method with an explicit availability indicator")
|
||||
public void hi() {
|
||||
|
||||
}
|
||||
public Availability customAvailabilityMethod() {
|
||||
return available ? Availability.available() : Availability.unavailable("customAvailabilityMethod");
|
||||
}
|
||||
|
||||
@ShellMethod(value = "some method with an explicit availability indicator", key = {"bonjour", "salut"})
|
||||
public void bonjour() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethodAvailability({"salut", "other"})
|
||||
public Availability availabilityForSeveralCommands() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityForSeveralCommands");
|
||||
}
|
||||
|
||||
@ShellMethod("a command whose availability indicator will come from wildcard")
|
||||
public void wild() {
|
||||
|
||||
}
|
||||
|
||||
@ShellMethodAvailability("*")
|
||||
private Availability availabilityFromWildcard() {
|
||||
return available ? Availability.available() : Availability.unavailable("availabilityFromWildcard");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorErrorMultipleExplicit() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(catalog);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("When set on a @ShellMethod method, the value of the @ShellMethodAvailability should be a single element")
|
||||
.hasMessageContaining("Found [one, two]")
|
||||
.hasMessageContaining("wrong()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorOnShellMethod {
|
||||
|
||||
@ShellMethodAvailability({"one", "two"})
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorWildcardNotAlone() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(catalog);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("When using '*' as a wildcard for ShellMethodAvailability, this can be the only value. Found [one, *]")
|
||||
.hasMessageContaining("availability()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorWildcardNotAlone {
|
||||
|
||||
@ShellMethodAvailability({"one", "*"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailabilityIndicatorAmbiguous() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
registrar.register(catalog);
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Found several @ShellMethodAvailability")
|
||||
.hasMessageContaining("wrong()")
|
||||
.hasMessageContaining("availability()")
|
||||
.hasMessageContaining("otherAvailability()");
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class WrongAvailabilityIndicatorAmbiguous {
|
||||
|
||||
@ShellMethodAvailability({"one", "wrong"})
|
||||
public Availability availability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethodAvailability({"bar", "wrong"})
|
||||
public Availability otherAvailability() {
|
||||
return Availability.available();
|
||||
}
|
||||
|
||||
@ShellMethod("foo")
|
||||
public void wrong() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrouping() {
|
||||
applicationContext = new AnnotationConfigApplicationContext(GroupOneCommands.class,
|
||||
GroupTwoCommands.class, GroupThreeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
|
||||
assertThat(catalog.getRegistrations().get("explicit1")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 1");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("explicit2")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 2");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("explicit3")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Explicit Group Method Level 3");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("implicit1")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Implicit Group Package Level 1");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("implicit2")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Group Two Commands");
|
||||
});
|
||||
assertThat(catalog.getRegistrations().get("implicit3")).satisfies(registration -> {
|
||||
assertThat(registration).isNotNull();
|
||||
assertThat(registration.getGroup()).isEqualTo("Explicit Group 3 Class Level");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteractionModeInteractive() {
|
||||
shellContext.setInteractionMode(InteractionMode.INTERACTIVE);
|
||||
applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
|
||||
assertThat(catalog.getRegistrations().get("foo1")).isNotNull();
|
||||
assertThat(catalog.getRegistrations().get("foo2")).isNull();
|
||||
assertThat(catalog.getRegistrations().get("foo3")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInteractionModeNonInteractive() {
|
||||
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
|
||||
applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class);
|
||||
registrar.setApplicationContext(applicationContext);
|
||||
registrar.register(catalog);
|
||||
|
||||
assertThat(catalog.getRegistrations().get("foo1")).isNull();
|
||||
assertThat(catalog.getRegistrations().get("foo2")).isNotNull();
|
||||
assertThat(catalog.getRegistrations().get("foo3")).isNotNull();
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class InteractionModeCommands {
|
||||
|
||||
@ShellMethod(value = "foo1", interactionMode = InteractionMode.INTERACTIVE)
|
||||
public void foo1() {
|
||||
}
|
||||
|
||||
@ShellMethod(value = "foo2", interactionMode = InteractionMode.NONINTERACTIVE)
|
||||
public void foo2() {
|
||||
}
|
||||
|
||||
@ShellMethod(value = "foo3")
|
||||
public void foo3() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 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.standard;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.reader.ParsedLine;
|
||||
import org.jline.reader.impl.DefaultParser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.ParameterMissingResolutionException;
|
||||
import org.springframework.shell.UnfinishedParameterResolutionException;
|
||||
import org.springframework.shell.Utils;
|
||||
import org.springframework.shell.ValueResult;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.shell.ValueResultAsserts.assertThat;
|
||||
import static org.springframework.util.ReflectionUtils.findMethod;
|
||||
|
||||
/**
|
||||
* Unit tests for DefaultParameterResolver.
|
||||
* @author Eric Bottard
|
||||
* @author Florent Biville
|
||||
*/
|
||||
public class StandardParameterResolverTest {
|
||||
|
||||
// private StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(), Collections.emptySet());
|
||||
|
||||
// Tests for resolution
|
||||
|
||||
@Test
|
||||
public void testParses() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
List<String> words = asList("--force --name --foo y".split(" "));
|
||||
ValueResult result0 = resolver.resolve(Utils.createMethodParameter(method, 0), words);
|
||||
assertThat(result0).hasValue(true).usesWords(0).notUsesWordsForValue();
|
||||
assertThat(result0.wordsUsed(words)).containsExactly("--force");
|
||||
|
||||
ValueResult result1 = resolver.resolve(Utils.createMethodParameter(method, 1), words);
|
||||
assertThat(result1).hasValue("--foo").usesWords(1, 2).usesWordsForValue(2);
|
||||
assertThat(result1.wordsUsed(words)).containsExactly("--name", "--foo");
|
||||
assertThat(result1.wordsUsedForValue(words)).containsExactly("--foo");
|
||||
|
||||
ValueResult result2 = resolver.resolve(Utils.createMethodParameter(method, 2), words);
|
||||
assertThat(result2).hasValue("y").usesWords(3).usesWordsForValue(3);
|
||||
assertThat(result2.wordsUsed(words)).containsExactly("y");
|
||||
|
||||
ValueResult result3 = resolver.resolve(Utils.createMethodParameter(method, 3), words);
|
||||
assertThat(result3).hasValue("last").notUsesWords().notUsesWordsForValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParsesWithMethodPrefix() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "prefixTest", String.class);
|
||||
|
||||
ValueResult result = resolver.resolve(Utils.createMethodParameter(method, 0),
|
||||
asList("-message abc".split(" ")));
|
||||
assertThat(result).hasValue("abc").usesWords(0, 1).usesWordsForValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameterSpecifiedTwiceViaDifferentAliases() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--force --name --foo y --bar x --baz z".split(" ")));
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Named parameter has been specified multiple times via '--bar, --baz'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameterSpecifiedTwiceViaSameKey() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--force --name --foo y --baz x --baz z".split(" ")));
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Parameter for '--baz' has already been specified");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTooMuchInput() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--foo hello --name bar --force --bar well leftover".split(" ")));
|
||||
}).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("the following could not be mapped to parameters: 'leftover'");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncompleteCommandResolution() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "shutdown", Remote.Delay.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--delay".split(" ")));
|
||||
}).isInstanceOf(UnfinishedParameterResolutionException.class)
|
||||
.hasMessageContaining("Error trying to resolve '--delay delay' using [--delay]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncompleteCommandResolutionBigArity() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "add", List.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
asList("--numbers 1 2".split(" ")));
|
||||
}).isInstanceOf(UnfinishedParameterResolutionException.class)
|
||||
.hasMessageContaining("Error trying to resolve '--numbers list list list' using [--numbers 1 2]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnresolvableArg() throws Exception {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
resolver.resolve(
|
||||
Utils.createMethodParameter(method, 1),
|
||||
asList("--foo hello --force --bar well".split(" ")));
|
||||
}).isInstanceOf(ParameterMissingResolutionException.class)
|
||||
.hasMessageContaining("Parameter '--name string' should be specified");
|
||||
}
|
||||
|
||||
// Tests for completion
|
||||
|
||||
@Test
|
||||
public void testParameterKeyNotYetSetAppearsInProposals() {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
List<String> completions = resolver.complete(
|
||||
Utils.createMethodParameter(method, 1),
|
||||
contextFor("")
|
||||
).stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("--name");
|
||||
completions = resolver.complete(
|
||||
Utils.createMethodParameter(method, 1),
|
||||
contextFor("--force ")
|
||||
).stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("--name");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameterKeyNotFullySpecified() {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
List<String> completions = resolver.complete(
|
||||
Utils.createMethodParameter(method, 1),
|
||||
contextFor("--na")
|
||||
).stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("--name");
|
||||
completions = resolver.complete(
|
||||
Utils.createMethodParameter(method, 1),
|
||||
contextFor("--force --na")
|
||||
).stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("--name");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoMoreAvailableParameters() {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "zap", boolean.class, String.class, String.class, String.class);
|
||||
List<String> completions = resolver.complete(
|
||||
Utils.createMethodParameter(method, 2), // trying to complete --foo
|
||||
contextFor("--name ") // but input is currently focused on --name
|
||||
).stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotTheRightTimeToCompleteThatParameter() {
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
Method method = findMethod(Remote.class, "shutdown", Remote.Delay.class);
|
||||
List<String> completions = resolver.complete(
|
||||
Utils.createMethodParameter(method, 0),
|
||||
contextFor("--delay 323")
|
||||
).stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValueCompletionWithNonDefaultArity() {
|
||||
Set<ValueProvider> valueProviders = new HashSet<>();
|
||||
valueProviders.add(new Remote.NumberValueProvider("12", "42", "7"));
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
valueProviders);
|
||||
|
||||
Method[] methods = {
|
||||
findMethod(org.springframework.shell.standard.Remote.class, "add", List.class),
|
||||
findMethod(org.springframework.shell.standard.Remote.class, "addAsArray", int[].class),
|
||||
};
|
||||
for (Method method : methods) {
|
||||
List<String> completions = resolver
|
||||
.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers ")).stream()
|
||||
.map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("12", "42", "7");
|
||||
|
||||
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 "))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("12", "7");
|
||||
|
||||
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 34 "))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).contains("12", "7");
|
||||
|
||||
completions = resolver.complete(Utils.createMethodParameter(method, 0), contextFor("--numbers 42 34 66 "))
|
||||
.stream().map(CompletionProposal::value).collect(Collectors.toList());
|
||||
assertThat(completions).isEmpty(); // All 3 have already been set
|
||||
}
|
||||
}
|
||||
|
||||
private CompletionContext contextFor(String input) {
|
||||
DefaultParser defaultParser = new DefaultParser();
|
||||
ParsedLine parsed = defaultParser.parse(input, input.length());
|
||||
List<String> words = parsed.words().stream().filter(w -> w.length() > 0).collect(Collectors.toList());
|
||||
|
||||
return new CompletionContext(words, parsed.wordIndex(), parsed.wordCursor());
|
||||
}
|
||||
}
|
||||
@@ -15,26 +15,15 @@
|
||||
*/
|
||||
package org.springframework.shell.standard.completion;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
import org.springframework.shell.standard.StandardParameterResolver;
|
||||
import org.springframework.shell.standard.completion.AbstractCompletions.CommandModel;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -43,30 +32,50 @@ public class AbstractCompletionsTests {
|
||||
@Test
|
||||
public void testBasicModelGeneration() {
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
StandardParameterResolver resolver = new StandardParameterResolver(new DefaultConversionService(),
|
||||
Collections.emptySet());
|
||||
parameterResolvers.add(resolver);
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
|
||||
TestCommands commands = new TestCommands();
|
||||
|
||||
Method method1 = ReflectionUtils.findMethod(TestCommands.class, "test1", String.class);
|
||||
Method method2 = ReflectionUtils.findMethod(TestCommands.class, "test2");
|
||||
Method method3 = ReflectionUtils.findMethod(TestCommands.class, "test3");
|
||||
Method method4 = ReflectionUtils.findMethod(TestCommands.class, "test4", String.class);
|
||||
CommandRegistration registration1 = CommandRegistration.builder()
|
||||
.command("test1")
|
||||
.withTarget()
|
||||
.method(commands, "test1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("param1")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
MethodTarget methodTarget1 = new MethodTarget(method1, commands, "help");
|
||||
MethodTarget methodTarget2 = new MethodTarget(method2, commands, "help");
|
||||
MethodTarget methodTarget3 = new MethodTarget(method3, commands, "help");
|
||||
MethodTarget methodTarget4 = new MethodTarget(method4, commands, "help");
|
||||
CommandRegistration registration2 = CommandRegistration.builder()
|
||||
.command("test2")
|
||||
.withTarget()
|
||||
.method(commands, "test2")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
commandRegistry.register("test1", methodTarget1);
|
||||
commandRegistry.register("test2", methodTarget2);
|
||||
commandRegistry.register("test3", methodTarget3);
|
||||
commandRegistry.register("test3 test4", methodTarget4);
|
||||
CommandRegistration registration3 = CommandRegistration.builder()
|
||||
.command("test3")
|
||||
.withTarget()
|
||||
.method(commands, "test3")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandRegistry, parameterResolvers);
|
||||
CommandRegistration registration4 = CommandRegistration.builder()
|
||||
.command("test3", "test4")
|
||||
.withTarget()
|
||||
.method(commands, "test4")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("param4")
|
||||
.and()
|
||||
.build();
|
||||
|
||||
commandCatalog.register(registration1);
|
||||
commandCatalog.register(registration2);
|
||||
commandCatalog.register(registration3);
|
||||
commandCatalog.register(registration4);
|
||||
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandCatalog);
|
||||
CommandModel commandModel = completions.testCommandModel();
|
||||
assertThat(commandModel.getCommands()).hasSize(3);
|
||||
assertThat(commandModel.getCommands().stream().map(c -> c.getMainCommand())).containsExactlyInAnyOrder("test1", "test2",
|
||||
@@ -92,9 +101,8 @@ public class AbstractCompletionsTests {
|
||||
@Test
|
||||
public void testBuilder() {
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandRegistry, parameterResolvers);
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
TestCompletions completions = new TestCompletions(resourceLoader, commandCatalog);
|
||||
|
||||
String result = completions.testBuilder()
|
||||
.attribute("x", "command")
|
||||
@@ -106,9 +114,8 @@ public class AbstractCompletionsTests {
|
||||
|
||||
private static class TestCompletions extends AbstractCompletions {
|
||||
|
||||
public TestCompletions(ResourceLoader resourceLoader, CommandRegistry commandRegistry,
|
||||
List<ParameterResolver> parameterResolvers) {
|
||||
super(resourceLoader, commandRegistry, parameterResolvers);
|
||||
public TestCompletions(ResourceLoader resourceLoader, CommandCatalog commandCatalog) {
|
||||
super(resourceLoader, commandCatalog);
|
||||
}
|
||||
|
||||
CommandModel testCommandModel() {
|
||||
|
||||
@@ -15,17 +15,16 @@
|
||||
*/
|
||||
package org.springframework.shell.standard.completion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.shell.ConfigurableCommandRegistry;
|
||||
import org.springframework.shell.ParameterResolver;
|
||||
import org.springframework.shell.context.DefaultShellContext;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.command.CommandContext;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -48,11 +47,71 @@ public class BashCompletionsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotError() {
|
||||
ConfigurableCommandRegistry commandRegistry = new ConfigurableCommandRegistry(new DefaultShellContext());
|
||||
List<ParameterResolver> parameterResolvers = new ArrayList<>();
|
||||
BashCompletions completions = new BashCompletions(context, commandRegistry, parameterResolvers);
|
||||
public void testNoCommands() {
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
BashCompletions completions = new BashCompletions(context, commandCatalog);
|
||||
String bash = completions.generate("root-command");
|
||||
assertThat(bash).contains("root-command");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandFromMethod() {
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
registerFromMethod(commandCatalog);
|
||||
BashCompletions completions = new BashCompletions(context, commandCatalog);
|
||||
String bash = completions.generate("root-command");
|
||||
System.out.println(bash);
|
||||
assertThat(bash).contains("root-command");
|
||||
assertThat(bash).contains("commands+=(\"testmethod1\")");
|
||||
assertThat(bash).contains("_root-command_testmethod1()");
|
||||
assertThat(bash).contains("two_word_flags+=(\"--arg1\")");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandFromFunction() {
|
||||
CommandCatalog commandCatalog = CommandCatalog.of();
|
||||
registerFromFunction(commandCatalog, "testmethod1");
|
||||
BashCompletions completions = new BashCompletions(context, commandCatalog);
|
||||
String bash = completions.generate("root-command");
|
||||
assertThat(bash).contains("root-command");
|
||||
assertThat(bash).contains("commands+=(\"testmethod1\")");
|
||||
assertThat(bash).contains("_root-command_testmethod1()");
|
||||
assertThat(bash).contains("two_word_flags+=(\"--arg1\")");
|
||||
}
|
||||
|
||||
private void registerFromMethod(CommandCatalog commandCatalog) {
|
||||
Pojo1 pojo1 = new Pojo1();
|
||||
CommandRegistration registration = CommandRegistration.builder()
|
||||
.command("testmethod1")
|
||||
.withTarget()
|
||||
.method(pojo1, "method1")
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.build();
|
||||
commandCatalog.register(registration);
|
||||
}
|
||||
|
||||
private void registerFromFunction(CommandCatalog commandCatalog, 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)
|
||||
.withTarget()
|
||||
.function(function)
|
||||
.and()
|
||||
.withOption()
|
||||
.longNames("arg1")
|
||||
.and()
|
||||
.build();
|
||||
commandCatalog.register(registration);
|
||||
}
|
||||
|
||||
protected static class Pojo1 {
|
||||
|
||||
void method1() {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user