From ef191e66f336f20b05a8e8db5f1edb69ba3f3f46 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sun, 4 Dec 2022 17:23:25 +0000 Subject: [PATCH] Add support for global help options - Essentially this commit registeres on default `--help` and `-h` options to every command and execution short circuits in presense of help options to help command. - Add Supplier as a bean which can be autowired registration beans. - Make this common bean customisable via CommandRegistrationCustomizer. - Change StandardMethodTargetRegistrar to use supplier so that annotated commands gets common customizations. - Change sample commands to use supplier. - Add new group, spring.shell.help to config props. - Docs changes - Fixes #582 - Fixes #585 --- .../boot/CommandCatalogAutoConfiguration.java | 29 +++ .../boot/CommandRegistrationCustomizer.java | 34 +++ .../shell/boot/SpringShellProperties.java | 64 +++++ .../boot/StandardAPIAutoConfiguration.java | 9 +- .../boot/SpringShellPropertiesTests.java | 26 +++ .../java/org/springframework/shell/Shell.java | 2 +- .../shell/command/CommandExecution.java | 74 +++++- .../shell/command/CommandRegistration.java | 221 +++++++++++++++++- .../command/CommandRegistrationTests.java | 20 ++ .../using-shell-commands-helpoptions.adoc | 41 ++++ ...sing-shell-commands-programmaticmodel.adoc | 33 ++- .../main/asciidoc/using-shell-commands.adoc | 2 + .../docs/CommandRegistrationBeanSnippets.java | 41 +++- ...ommandRegistrationHelpOptionsSnippets.java | 40 ++++ .../shell/samples/e2e/ArityCommands.java | 6 +- .../samples/e2e/DefaultValueCommands.java | 18 +- .../samples/e2e/ErrorHandlingCommands.java | 6 +- .../shell/samples/e2e/ExitCodeCommands.java | 6 +- .../shell/samples/e2e/HelpOptionCommands.java | 85 +++++++ .../shell/samples/e2e/HiddenCommands.java | 6 +- .../e2e/InteractiveCompletionCommands.java | 5 +- .../shell/samples/e2e/OptionTypeCommands.java | 21 +- .../samples/e2e/OptionalValueCommands.java | 6 +- .../samples/e2e/RequiredValueCommands.java | 6 +- .../samples/e2e/ValidatedValueCommands.java | 6 +- .../StandardMethodTargetRegistrar.java | 12 +- .../StandardMethodTargetRegistrarTests.java | 32 +-- 27 files changed, 773 insertions(+), 78 deletions(-) create mode 100644 spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandRegistrationCustomizer.java create mode 100644 spring-shell-docs/src/main/asciidoc/using-shell-commands-helpoptions.adoc create mode 100644 spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationHelpOptionsSnippets.java create mode 100644 spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HelpOptionCommands.java diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java index 05fdc339..0deb0346 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandCatalogAutoConfiguration.java @@ -16,19 +16,23 @@ package org.springframework.shell.boot; import java.util.List; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.shell.MethodTargetRegistrar; +import org.springframework.shell.boot.SpringShellProperties.Help; import org.springframework.shell.command.CommandCatalog; import org.springframework.shell.command.CommandCatalogCustomizer; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.command.CommandResolver; @AutoConfiguration +@EnableConfigurationProperties(SpringShellProperties.class) public class CommandCatalogAutoConfiguration { @Bean @@ -55,4 +59,29 @@ public class CommandCatalogAutoConfiguration { }); }; } + + @Bean + public CommandRegistrationCustomizer helpOptionsCommandRegistrationCustomizer(SpringShellProperties properties) { + return registration -> { + Help help = properties.getHelp(); + if (help.isEnabled()) { + registration.withHelpOptions() + .enabled(true) + .longNames(help.getLongNames()) + .shortNames(help.getShortNames()) + .command(help.getCommand()); + } + }; + } + + @Bean + @ConditionalOnMissingBean + public Supplier commandRegistrationBuilderSupplier( + ObjectProvider customizerProvider) { + return () -> { + CommandRegistration.Builder builder = CommandRegistration.builder(); + customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + }; + } } diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandRegistrationCustomizer.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandRegistrationCustomizer.java new file mode 100644 index 00000000..59731217 --- /dev/null +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CommandRegistrationCustomizer.java @@ -0,0 +1,34 @@ +/* + * 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 org.springframework.shell.command.CommandRegistration; + +/** + * Callback interface that can be used to customize a {@link CommandRegistration.Builder}. + * + * @author Janne Valkealahti + */ +@FunctionalInterface +public interface CommandRegistrationCustomizer { + + /** + * Callback to customize a {@link CommandRegistration.Builder} instance. + * + * @param commandRegistrationBuilder the command registration builder to customize + */ + void customize(CommandRegistration.Builder commandRegistrationBuilder); +} diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java index cf523ca2..72be18d5 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java @@ -32,6 +32,7 @@ public class SpringShellProperties { private Noninteractive noninteractive = new Noninteractive(); private Theme theme = new Theme(); private Command command = new Command(); + private Help help = new Help(); public void setConfig(Config config) { this.config = config; @@ -89,6 +90,14 @@ public class SpringShellProperties { this.command = command; } + public void setHelp(Help help) { + this.help = help; + } + + public Help getHelp() { + return help; + } + public static class Config { private String env; @@ -495,4 +504,59 @@ public class SpringShellProperties { this.showGitCommitTime = showGitCommitTime; } } + + public static class Help { + + /** + * Command to call when presense of help option is detected. + */ + private String command = "help"; + + /** + * Long style help option, without a prefix "--". + */ + private String[] longNames = new String[] { "help" }; + + /** + * Short style help option, without a prefix "-". + */ + private Character[] shortNames = new Character[] { 'h' }; + + /** + * Whether to enable help options for commands. + */ + private boolean enabled = true; + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public String[] getLongNames() { + return longNames; + } + + public void setLongNames(String[] longNames) { + this.longNames = longNames; + } + + public Character[] getShortNames() { + return shortNames; + } + + public void setShortNames(Character[] shortNames) { + this.shortNames = shortNames; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } } diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardAPIAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardAPIAutoConfiguration.java index d227907c..841c1005 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardAPIAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardAPIAutoConfiguration.java @@ -16,10 +16,14 @@ package org.springframework.shell.boot; +import java.util.function.Supplier; + import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.shell.MethodTargetRegistrar; import org.springframework.shell.command.CommandCatalog; +import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.standard.CommandValueProvider; import org.springframework.shell.standard.EnumValueProvider; import org.springframework.shell.standard.FileValueProvider; @@ -50,7 +54,8 @@ public class StandardAPIAutoConfiguration { } @Bean - public MethodTargetRegistrar standardMethodTargetResolver() { - return new StandardMethodTargetRegistrar(); + public MethodTargetRegistrar standardMethodTargetResolver(ApplicationContext applicationContext, + Supplier builder) { + return new StandardMethodTargetRegistrar(applicationContext, builder); } } diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java index 0da41ffc..67b299dd 100644 --- a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java @@ -63,6 +63,10 @@ public class SpringShellPropertiesTests { assertThat(properties.getCommand().getVersion().isShowGitCommitId()).isFalse(); assertThat(properties.getCommand().getVersion().isShowGitShortCommitId()).isFalse(); assertThat(properties.getCommand().getVersion().isShowGitCommitTime()).isFalse(); + assertThat(properties.getHelp().isEnabled()).isTrue(); + assertThat(properties.getHelp().getCommand()).isEqualTo("help"); + assertThat(properties.getHelp().getLongNames()).containsExactly("help"); + assertThat(properties.getHelp().getShortNames()).containsExactly('h'); }); } @@ -99,6 +103,10 @@ public class SpringShellPropertiesTests { .withPropertyValues("spring.shell.command.version.show-git-commit-id=true") .withPropertyValues("spring.shell.command.version.show-git-short-commit-id=true") .withPropertyValues("spring.shell.command.version.show-git-commit-time=true") + .withPropertyValues("spring.shell.help.enabled=false") + .withPropertyValues("spring.shell.help.command=fake") + .withPropertyValues("spring.shell.help.long-names=fake") + .withPropertyValues("spring.shell.help.short-names=f") .withUserConfiguration(Config1.class) .run((context) -> { SpringShellProperties properties = context.getBean(SpringShellProperties.class); @@ -132,6 +140,24 @@ public class SpringShellPropertiesTests { assertThat(properties.getCommand().getVersion().isShowGitCommitId()).isTrue(); assertThat(properties.getCommand().getVersion().isShowGitShortCommitId()).isTrue(); assertThat(properties.getCommand().getVersion().isShowGitCommitTime()).isTrue(); + assertThat(properties.getHelp().isEnabled()).isFalse(); + assertThat(properties.getHelp().getCommand()).isEqualTo("fake"); + assertThat(properties.getHelp().getLongNames()).containsExactly("fake"); + assertThat(properties.getHelp().getShortNames()).containsExactly('f'); + }); + } + + + @Test + public void essentiallyUnset() { + this.contextRunner + .withPropertyValues("spring.shell.help.long-names=") + .withPropertyValues("spring.shell.help.short-names=") + .withUserConfiguration(Config1.class) + .run((context) -> { + SpringShellProperties properties = context.getBean(SpringShellProperties.class); + assertThat(properties.getHelp().getLongNames()).isEmpty(); + assertThat(properties.getHelp().getShortNames()).isEmpty(); }); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java index bac43a0a..2301b920 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java @@ -236,7 +236,7 @@ public class Shell { CommandExecution execution = CommandExecution.of( argumentResolvers != null ? argumentResolvers.getResolvers() : null, validator, terminal, - conversionService); + conversionService, commandRegistry); List commandExceptionResolvers = commandRegistration.get().getExceptionResolvers(); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java index 192bdd44..f588dae4 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java @@ -15,12 +15,12 @@ */ package org.springframework.shell.command; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import jakarta.validation.Validator; - import org.jline.terminal.Terminal; import org.springframework.core.MethodParameter; @@ -33,10 +33,12 @@ import org.springframework.shell.Availability; import org.springframework.shell.CommandNotCurrentlyAvailable; import org.springframework.shell.command.CommandParser.CommandParserException; import org.springframework.shell.command.CommandParser.CommandParserResults; +import org.springframework.shell.command.CommandRegistration.HelpOptionInfo; 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; +import org.springframework.util.ObjectUtils; /** * Interface to evaluate a result from a command with an arguments. @@ -61,7 +63,7 @@ public interface CommandExecution { * @return default command execution */ public static CommandExecution of(List resolvers) { - return new DefaultCommandExecution(resolvers, null, null, null); + return new DefaultCommandExecution(resolvers, null, null, null, null); } /** @@ -75,7 +77,21 @@ public interface CommandExecution { */ public static CommandExecution of(List resolvers, Validator validator, Terminal terminal, ConversionService conversionService) { - return new DefaultCommandExecution(resolvers, validator, terminal, conversionService); + return new DefaultCommandExecution(resolvers, validator, terminal, conversionService, null); + } + + /** + * Gets an instance of a default {@link CommandExecution}. + * + * @param resolvers the handler method argument resolvers + * @param validator the validator + * @param terminal the terminal + * @param conversionService the conversion services + * @return default command execution + */ + public static CommandExecution of(List resolvers, Validator validator, + Terminal terminal, ConversionService conversionService, CommandCatalog commandCatalog) { + return new DefaultCommandExecution(resolvers, validator, terminal, conversionService, commandCatalog); } /** @@ -87,13 +103,15 @@ public interface CommandExecution { private Validator validator; private Terminal terminal; private ConversionService conversionService; + private CommandCatalog commandCatalog; public DefaultCommandExecution(List resolvers, Validator validator, - Terminal terminal, ConversionService conversionService) { + Terminal terminal, ConversionService conversionService, CommandCatalog commandCatalog) { this.resolvers = resolvers; this.validator = validator; this.terminal = terminal; this.conversionService = conversionService; + this.commandCatalog = commandCatalog; } public Object evaluate(CommandRegistration registration, String[] args) { @@ -107,15 +125,59 @@ public interface CommandExecution { CommandParser parser = CommandParser.of(conversionService); CommandParserResults results = parser.parse(options, args); + // check help options to short circuit + boolean handleHelpOption = false; + HelpOptionInfo helpOption = registration.getHelpOption(); + if (helpOption.isEnabled() && helpOption.getCommand() != null && (!ObjectUtils.isEmpty(helpOption.getLongNames()) || !ObjectUtils.isEmpty(helpOption.getShortNames()))) { + handleHelpOption = results.results().stream() + .filter(cpr -> { + boolean present = false; + if (helpOption.getLongNames() != null) { + present = Arrays.asList(cpr.option().getLongNames()).stream() + .filter(ln -> ObjectUtils.containsElement(helpOption.getLongNames(), ln)) + .findFirst() + .isPresent(); + } + if (present) { + return true; + } + if (helpOption.getShortNames() != null) { + present = Arrays.asList(cpr.option().getShortNames()).stream() + .filter(sn -> ObjectUtils.containsElement(helpOption.getShortNames(), sn)) + .findFirst() + .isPresent(); + } + return present; + }) + .findFirst() + .isPresent(); + } + + // if needed switch registration to help command if we're short circuiting + CommandRegistration usedRegistration; + if (handleHelpOption) { + String command = registration.getCommand(); + CommandParser helpParser = CommandParser.of(conversionService); + CommandRegistration helpCommandRegistration = commandCatalog.getRegistrations() + .get(registration.getHelpOption().getCommand()); + List helpOptions = helpCommandRegistration.getOptions(); + CommandParserResults helpResults = helpParser.parse(helpOptions, new String[] { "--command", command }); + results = helpResults; + usedRegistration = helpCommandRegistration; + } + else { + usedRegistration = registration; + } + if (!results.errors().isEmpty()) { throw new CommandParserExceptionsException("Command parser resulted errors", results.errors()); } - CommandContext ctx = CommandContext.of(args, results, terminal, registration); + CommandContext ctx = CommandContext.of(args, results, terminal, usedRegistration); Object res = null; - TargetInfo targetInfo = registration.getTarget(); + TargetInfo targetInfo = usedRegistration.getTarget(); // pick the target to execute if (targetInfo.getTargetType() == TargetType.FUNCTION) { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java index 69eb4cd2..0310263e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java @@ -121,6 +121,13 @@ public interface CommandRegistration { */ List getExceptionResolvers(); + /** + * Gets a help option info. + * + * @return the help option info + */ + HelpOptionInfo getHelpOption(); + /** * Gets a new instance of a {@link Builder}. * @@ -475,6 +482,125 @@ public interface CommandRegistration { Builder and(); } + public interface HelpOptionInfo { + + /** + * Gets whether help options are enabled. + * + * @return whether help options are enabled + */ + boolean isEnabled(); + + /** + * Gets long names options for help. + * + * @return long names options for help + */ + String[] getLongNames(); + + /** + * Gets short names options for help. + * + * @return short names options for help + */ + Character[] getShortNames(); + + /** + * Gets command for help. + * + * @return command for help + */ + String getCommand(); + + static HelpOptionInfo of() { + return of(false, null, null, null); + } + + static HelpOptionInfo of(boolean enabled, String[] longNames, Character[] shortNames, String command) { + return new DefaultHelpOptionInfo(enabled, longNames, shortNames, command); + } + + static class DefaultHelpOptionInfo implements HelpOptionInfo { + + private final String command; + private final String[] longNames; + private final Character[] shortNames; + private final boolean enabled; + + public DefaultHelpOptionInfo(boolean enabled, String[] longNames, Character[] shortNames, String command) { + this.command = command; + this.longNames = longNames; + this.shortNames = shortNames; + this.enabled = enabled; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public String[] getLongNames() { + return longNames; + } + + @Override + public Character[] getShortNames() { + return shortNames; + } + + @Override + public String getCommand() { + return command; + } + } + } + + /** + * Spec defining help options. + */ + public interface HelpOptionsSpec { + + /** + * Whether help options are enabled. + * + * @param enabled the enabled flag + * @return a help option for chaining + */ + HelpOptionsSpec enabled(boolean enabled); + + /** + * Sets long names options for help. + * + * @param longNames the long names + * @return a help option for chaining + */ + HelpOptionsSpec longNames(String... longNames); + + /** + * Sets short names options for help. + * + * @param shortNames the short names + * @return a help option for chaining + */ + HelpOptionsSpec shortNames(Character... shortNames); + + /** + * Sets command used for help. + * + * @param command the command + * @return a help option for chaining + */ + HelpOptionsSpec command(String command); + + /** + * Return a builder for chaining. + * + * @return a builder for chaining + */ + Builder and(); + } + /** * Builder interface for {@link CommandRegistration}. */ @@ -575,6 +701,13 @@ public interface CommandRegistration { */ ErrorHandlingSpec withErrorHandling(); + /** + * Define help options what this command should use. + * + * @return help options spec for chaining + */ + HelpOptionsSpec withHelpOptions(); + /** * Builds a {@link CommandRegistration}. * @@ -884,6 +1017,57 @@ public interface CommandRegistration { } } + static class DefaultHelpOptionsSpec implements HelpOptionsSpec { + + private BaseBuilder builder; + private String command; + private String[] longNames; + private Character[] shortNames; + private boolean enabled = true; + + DefaultHelpOptionsSpec(BaseBuilder builder) { + this.builder = builder; + } + + DefaultHelpOptionsSpec(BaseBuilder otherBuilder, DefaultHelpOptionsSpec otherSpec) { + this.builder = otherBuilder; + this.builder.helpOptionsSpec = this; + this.command = otherSpec.command; + this.longNames = otherSpec.longNames.clone(); + this.shortNames = otherSpec.shortNames.clone(); + this.enabled = otherSpec.enabled; + } + + @Override + public HelpOptionsSpec command(String command) { + this.command = command; + return this; + } + + @Override + public HelpOptionsSpec longNames(String... longNames) { + this.longNames = longNames; + return this; + } + + @Override + public HelpOptionsSpec shortNames(Character... shortNames) { + this.shortNames = shortNames; + return this; + } + + @Override + public HelpOptionsSpec enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + @Override + public Builder and() { + return builder; + } + } + static class DefaultCommandRegistration implements CommandRegistration { private String command; @@ -897,11 +1081,12 @@ public interface CommandRegistration { private List aliasSpecs; private DefaultExitCodeSpec exitCodeSpec; private DefaultErrorHandlingSpec errorHandlingSpec; + private DefaultHelpOptionsSpec helpOptionsSpec; public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group, boolean hidden, String description, Supplier availability, List optionSpecs, DefaultTargetSpec targetSpec, List aliasSpecs, - DefaultExitCodeSpec exitCodeSpec, DefaultErrorHandlingSpec errorHandlingSpec) { + DefaultExitCodeSpec exitCodeSpec, DefaultErrorHandlingSpec errorHandlingSpec, DefaultHelpOptionsSpec helpOptionsSpec) { this.command = commandArrayToName(commands); this.interactionMode = interactionMode; this.group = group; @@ -913,6 +1098,7 @@ public interface CommandRegistration { this.aliasSpecs = aliasSpecs; this.exitCodeSpec = exitCodeSpec; this.errorHandlingSpec = errorHandlingSpec; + this.helpOptionsSpec = helpOptionsSpec; } @Override @@ -947,11 +1133,17 @@ public interface CommandRegistration { @Override public List getOptions() { - return optionSpecs.stream() + List options = optionSpecs.stream() .map(o -> CommandOption.of(o.getLongNames(), o.getShortNames(), o.getDescription(), o.getType(), o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax(), o.getLabel(), o.getCompletion())) .collect(Collectors.toList()); + if (helpOptionsSpec != null) { + String[] longNames = helpOptionsSpec.longNames != null ? helpOptionsSpec.longNames : null; + Character[] shortNames = helpOptionsSpec.shortNames != null ? helpOptionsSpec.shortNames : null; + options.add(CommandOption.of(longNames, shortNames, "help for " + command)); + } + return options; } @Override @@ -997,6 +1189,17 @@ public interface CommandRegistration { } } + @Override + public HelpOptionInfo getHelpOption() { + if (this.helpOptionsSpec == null) { + return HelpOptionInfo.of(); + } + else { + return HelpOptionInfo.of(helpOptionsSpec.enabled, helpOptionsSpec.longNames, helpOptionsSpec.shortNames, + helpOptionsSpec.command); + } + } + private static String commandArrayToName(String[] commands) { return Arrays.asList(commands).stream() .flatMap(c -> Stream.of(c.split(" "))) @@ -1007,10 +1210,9 @@ public interface CommandRegistration { } static class DefaultBuilder extends BaseBuilder { - } - static class BaseBuilder implements Builder { + static abstract class BaseBuilder implements Builder { private String[] commands; private InteractionMode interactionMode = InteractionMode.ALL; @@ -1023,6 +1225,7 @@ public interface CommandRegistration { private DefaultTargetSpec targetSpec; private DefaultExitCodeSpec exitCodeSpec; private DefaultErrorHandlingSpec errorHandlingSpec; + private DefaultHelpOptionsSpec helpOptionsSpec; @Override public Builder command(String... commands) { @@ -1106,13 +1309,21 @@ public interface CommandRegistration { return spec; } + @Override + public HelpOptionsSpec withHelpOptions() { + if (this.helpOptionsSpec == null) { + this.helpOptionsSpec = new DefaultHelpOptionsSpec(this); + } + return this.helpOptionsSpec; + } + @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, group, hidden, description, availability, - optionSpecs, targetSpec, aliasSpecs, exitCodeSpec, errorHandlingSpec); + optionSpecs, targetSpec, aliasSpecs, exitCodeSpec, errorHandlingSpec, helpOptionsSpec); } } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java index a0bbf2d5..04957e60 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandRegistrationTests.java @@ -523,4 +523,24 @@ public class CommandRegistrationTests extends AbstractCommandTests { .build(); assertThat(registration.isHidden()).isTrue(); } + + @Test + public void testHelpOption() { + CommandRegistration registration = CommandRegistration.builder() + .command("command1") + .withHelpOptions() + .enabled(true) + .longNames(new String[] { "help" }) + .shortNames(new Character[] { 'h' }) + .command("help") + .and() + .withTarget() + .function(function1) + .and() + .build(); + + assertThat(registration.getOptions()).hasSize(1); + assertThat(registration.getOptions().get(0).getLongNames()).containsExactly("help"); + assertThat(registration.getOptions().get(0).getShortNames()).containsExactly('h'); + } } diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-commands-helpoptions.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-commands-helpoptions.adoc new file mode 100644 index 00000000..a3418782 --- /dev/null +++ b/spring-shell-docs/src/main/asciidoc/using-shell-commands-helpoptions.adoc @@ -0,0 +1,41 @@ +[[commands-helpoptions]] +=== Help Options +ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs] + +_Spring Shell_ has a build-in `help` command but not all favour getting command help +from it as you always need to call it with arguments for target command. It's +common in many cli frameworks for every command having options _--help_ and _-h_ +to print out command help. + +Default functionality is that every command will get modified to have options +_--help_ and _-h_, which if present in a given command will automatically +short circuit command execution into a existing `help` command regardless +what other command-line options is typed. + +Below example shows its default settings. + +==== +[source, java, indent=0] +---- +include::{snippets}/CommandRegistrationHelpOptionsSnippets.java[tag=defaults] +---- +==== + +It is possible to change default behaviour via configuration options. + +==== +[source, yaml] +---- +spring: + shell: + help: + enabled: true + long-names: help + short-names: h + command: help +---- +==== + +NOTE: Commands defined programmationally or via annotations will automatically add +help options. With annotation model you can only turn things off globally, programmatic +model gives option to modify settings per command. diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-commands-programmaticmodel.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-commands-programmaticmodel.adoc index 625693d4..f2a0b0cd 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-commands-programmaticmodel.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-commands-programmaticmodel.adoc @@ -1,11 +1,40 @@ ==== Programmatic Model ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs] -In the programmatic model, `CommandRegistration` is defined as a `@Bean`, and it is automatically registered: +In the programmatic model, `CommandRegistration` can be defined as a `@Bean` +and it will be automatically registered. ==== [source, java, indent=0] ---- -include::{snippets}/CommandRegistrationBeanSnippets.java[tag=snippet1] +include::{snippets}/CommandRegistrationBeanSnippets.java[tag=plain] +---- +==== + +If all your commands have something in common, an instance of +a _Supplier_ is created which can +be autowired. Default implementation of this supplier returns +a new builder so you don't need to worry about its internal state. + +IMPORTANT: Commands registered programmatically automatically +add _help options_ mentioned in <>. + +If bean of this supplier type is defined then auto-configuration +will back off giving you an option to redefine default functionality. + +==== +[source, java, indent=0] +---- +include::{snippets}/CommandRegistrationBeanSnippets.java[tag=fromsupplier] +---- +==== + +`CommandRegistrationCustomizer` beans can be defined if you want to centrally +modify builder instance given you by supplier mentioned above. + +==== +[source, java, indent=0] +---- +include::{snippets}/CommandRegistrationBeanSnippets.java[tag=customizer] ---- ==== diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-commands.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-commands.adoc index 08a216b9..2a4187c0 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-commands.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-commands.adoc @@ -15,6 +15,8 @@ include::using-shell-commands-exceptionhandling.adoc[] include::using-shell-commands-hidden.adoc[] +include::using-shell-commands-helpoptions.adoc[] + include::using-shell-commands-interactionmode.adoc[] include::using-shell-commands-builtin.adoc[] diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationBeanSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationBeanSnippets.java index aa8108d1..c8722e50 100644 --- a/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationBeanSnippets.java +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationBeanSnippets.java @@ -15,17 +15,44 @@ */ package org.springframework.shell.docs; +import java.util.function.Supplier; + import org.springframework.context.annotation.Bean; +import org.springframework.shell.boot.CommandRegistrationCustomizer; import org.springframework.shell.command.CommandRegistration; public class CommandRegistrationBeanSnippets { - // tag::snippet1[] - @Bean - CommandRegistration commandRegistration() { - return CommandRegistration.builder() - .command("mycommand") - .build(); + class Dump1 { + // tag::plain[] + @Bean + CommandRegistration commandRegistration() { + return CommandRegistration.builder() + .command("mycommand") + .build(); + } + // end::plain[] + } + + class Dump2 { + // tag::fromsupplier[] + @Bean + CommandRegistration commandRegistration(Supplier builder) { + return builder.get() + .command("mycommand") + .build(); + } + // end::fromsupplier[] + } + + class Dump3 { + // tag::customizer[] + @Bean + CommandRegistrationCustomizer commandRegistrationCustomizerExample() { + return builder -> { + // customize instance of CommandRegistration.Builder + }; + } + // end::customizer[] } - // end::snippet1[] } diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationHelpOptionsSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationHelpOptionsSnippets.java new file mode 100644 index 00000000..188d8c1f --- /dev/null +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/CommandRegistrationHelpOptionsSnippets.java @@ -0,0 +1,40 @@ +/* + * 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.docs; + +import org.springframework.context.annotation.Bean; +import org.springframework.shell.command.CommandRegistration; + +class CommandRegistrationHelpOptionsSnippets { + + class Dump1 { + // tag::defaults[] + @Bean + CommandRegistration commandRegistration() { + return CommandRegistration.builder() + .command("mycommand") + .withHelpOptions() + .enabled(true) + .longNames("help") + .shortNames('h') + .command("help") + .and() + .build(); + } + // end::defaults[] + } + +} diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java index 4eda48d4..50407fa3 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ArityCommands.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.samples.e2e; +import java.util.function.Supplier; + import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.command.CommandRegistration.OptionArity; @@ -38,8 +40,8 @@ public class ArityCommands extends BaseE2ECommands { } @Bean - public CommandRegistration testBooleanArity1DefaultTrueRegistration() { - return CommandRegistration.builder() + public CommandRegistration testBooleanArity1DefaultTrueRegistration(Supplier builder) { + return builder.get() .command(REG, "boolean-arity1-default-true") .group(GROUP) .withOption() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/DefaultValueCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/DefaultValueCommands.java index 5e88bb87..9b26a780 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/DefaultValueCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/DefaultValueCommands.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.samples.e2e; +import java.util.function.Supplier; + import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.standard.ShellComponent; @@ -37,8 +39,8 @@ public class DefaultValueCommands extends BaseE2ECommands { } @Bean - public CommandRegistration testDefaultValueRegistration() { - return CommandRegistration.builder() + public CommandRegistration testDefaultValueRegistration(Supplier builder) { + return builder.get() .command(REG, "default-value") .group(GROUP) .withOption() @@ -62,8 +64,8 @@ public class DefaultValueCommands extends BaseE2ECommands { } @Bean - public CommandRegistration testDefaultValueBoolean1Registration() { - return CommandRegistration.builder() + public CommandRegistration testDefaultValueBoolean1Registration(Supplier builder) { + return builder.get() .command(REG, "default-value-boolean1") .group(GROUP) .withOption() @@ -88,8 +90,8 @@ public class DefaultValueCommands extends BaseE2ECommands { } @Bean - public CommandRegistration testDefaultValueBoolean2Registration() { - return CommandRegistration.builder() + public CommandRegistration testDefaultValueBoolean2Registration(Supplier builder) { + return builder.get() .command(REG, "default-value-boolean2") .group(GROUP) .withOption() @@ -114,8 +116,8 @@ public class DefaultValueCommands extends BaseE2ECommands { } @Bean - public CommandRegistration testDefaultValueBoolean3Registration() { - return CommandRegistration.builder() + public CommandRegistration testDefaultValueBoolean3Registration(Supplier builder) { + return builder.get() .command(REG, "default-value-boolean3") .group(GROUP) .withOption() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ErrorHandlingCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ErrorHandlingCommands.java index 322930fc..7bc9b7ee 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ErrorHandlingCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ErrorHandlingCommands.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.samples.e2e; +import java.util.function.Supplier; + import org.springframework.boot.ExitCodeGenerator; import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; @@ -31,8 +33,8 @@ import org.springframework.shell.standard.ShellComponent; public class ErrorHandlingCommands extends BaseE2ECommands { @Bean - public CommandRegistration testErrorHandlingRegistration() { - return CommandRegistration.builder() + public CommandRegistration testErrorHandlingRegistration(Supplier builder) { + return builder.get() .command(REG, "error-handling") .group(GROUP) .withOption() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ExitCodeCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ExitCodeCommands.java index 67fb28b3..230776ba 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ExitCodeCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ExitCodeCommands.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.samples.e2e; +import java.util.function.Supplier; + import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.standard.ShellComponent; @@ -28,8 +30,8 @@ import org.springframework.shell.standard.ShellComponent; public class ExitCodeCommands extends BaseE2ECommands { @Bean - public CommandRegistration testExitCodeRegistration() { - return CommandRegistration.builder() + public CommandRegistration testExitCodeRegistration(Supplier builder) { + return builder.get() .command(REG, "exit-code") .group(GROUP) .withOption() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HelpOptionCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HelpOptionCommands.java new file mode 100644 index 00000000..5bdea5fc --- /dev/null +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HelpOptionCommands.java @@ -0,0 +1,85 @@ +/* + * 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.e2e; + +import java.util.function.Supplier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.shell.command.CommandRegistration; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellOption; + +@ShellComponent +public class HelpOptionCommands extends BaseE2ECommands { + + @Autowired + Supplier builder; + + @ShellMethod(key = LEGACY_ANNO + "help-option-default", group = GROUP) + public String testHelpOptionDefault( + @ShellOption(defaultValue = "hi") String arg1 + ) { + return "Hello " + arg1; + } + + @Bean + public CommandRegistration testHelpOptionDefaultRegistration() { + return builder.get() + .command(REG, "help-option-default") + .group(GROUP) + .withOption() + .longNames("arg1") + .defaultValue("hi") + .and() + .withTarget() + .function(ctx -> { + String arg1 = ctx.getOptionValue("arg1"); + return "Hello " + arg1; + }) + .and() + .build(); + } + + // @ShellMethod(key = LEGACY_ANNO + "help-option-exists", group = GROUP) + // public String testHelpOptionExists( + // @ShellOption(defaultValue = "hi") String help + // ) { + // return "Hello " + help; + // } + + @Bean + public CommandRegistration testHelpOptionExistsRegistration() { + return builder.get() + .command(REG, "help-option-exists") + .group(GROUP) + .withOption() + .longNames("help") + .defaultValue("hi") + .and() + .withHelpOptions() + .longNames("myhelp") + .and() + .withTarget() + .function(ctx -> { + String arg1 = ctx.getOptionValue("help"); + return "Hello " + arg1; + }) + .and() + .build(); + } +} diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HiddenCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HiddenCommands.java index 92f3afcd..892d90af 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HiddenCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/HiddenCommands.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.samples.e2e; +import java.util.function.Supplier; + import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.standard.ShellComponent; @@ -23,8 +25,8 @@ import org.springframework.shell.standard.ShellComponent; public class HiddenCommands extends BaseE2ECommands { @Bean - public CommandRegistration testHidden1Registration() { - return CommandRegistration.builder() + public CommandRegistration testHidden1Registration(Supplier builder) { + return builder.get() .command(REG, "hidden-1") .group(GROUP) .hidden() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/InteractiveCompletionCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/InteractiveCompletionCommands.java index 3efa06de..757a8ba1 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/InteractiveCompletionCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/InteractiveCompletionCommands.java @@ -17,6 +17,7 @@ package org.springframework.shell.samples.e2e; import java.util.Arrays; import java.util.List; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.springframework.context.annotation.Bean; @@ -40,10 +41,10 @@ public class InteractiveCompletionCommands extends BaseE2ECommands { } @Bean - CommandRegistration testInteractiveCompletion1Registration() { + CommandRegistration testInteractiveCompletion1Registration(Supplier builder) { Test1ValuesProvider test1ValuesProvider = new Test1ValuesProvider(); Test2ValuesProvider test2ValuesProvider = new Test2ValuesProvider(); - return CommandRegistration.builder() + return builder.get() .command(REG, "interactive-completion-1") .group(GROUP) .withOption() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionTypeCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionTypeCommands.java index 9c0fca1a..a463784c 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionTypeCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionTypeCommands.java @@ -16,6 +16,7 @@ package org.springframework.shell.samples.e2e; import java.io.PrintWriter; +import java.util.function.Supplier; import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; @@ -32,8 +33,8 @@ import org.springframework.shell.standard.ShellOption; public class OptionTypeCommands extends BaseE2ECommands { @Bean - public CommandRegistration testOptionTypeRegistration() { - return CommandRegistration.builder() + public CommandRegistration testOptionTypeRegistration(Supplier builder) { + return builder.get() .command(REG, "option-type") .group(GROUP) .withOption() @@ -76,8 +77,8 @@ public class OptionTypeCommands extends BaseE2ECommands { } @Bean - public CommandRegistration optionTypeStringRegistration() { - return CommandRegistration.builder() + public CommandRegistration optionTypeStringRegistration(Supplier builder) { + return builder.get() .command(REG, "option-type-string") .group(GROUP) .withOption() @@ -112,8 +113,8 @@ public class OptionTypeCommands extends BaseE2ECommands { } @Bean - public CommandRegistration optionTypeBooleanRegistration() { - return CommandRegistration.builder() + public CommandRegistration optionTypeBooleanRegistration(Supplier builder) { + return builder.get() .command(REG, "option-type-boolean") .group(GROUP) .withOption() @@ -172,8 +173,8 @@ public class OptionTypeCommands extends BaseE2ECommands { } @Bean - public CommandRegistration optionTypeIntegerRegistration() { - return CommandRegistration.builder() + public CommandRegistration optionTypeIntegerRegistration(Supplier builder) { + return builder.get() .command(REG, "option-type-integer") .group(GROUP) .withOption() @@ -212,8 +213,8 @@ public class OptionTypeCommands extends BaseE2ECommands { } @Bean - public CommandRegistration optionTypeEnumRegistration() { - return CommandRegistration.builder() + public CommandRegistration optionTypeEnumRegistration(Supplier builder) { + return builder.get() .command(REG, "option-type-enum") .group(GROUP) .withOption() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionalValueCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionalValueCommands.java index cf9fe952..c2792eff 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionalValueCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/OptionalValueCommands.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.samples.e2e; +import java.util.function.Supplier; + import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.standard.ShellComponent; @@ -37,8 +39,8 @@ public class OptionalValueCommands extends BaseE2ECommands { } @Bean - public CommandRegistration testOptionalValueRegistration() { - return CommandRegistration.builder() + public CommandRegistration testOptionalValueRegistration(Supplier builder) { + return builder.get() .command(REG, "optional-value") .group(GROUP) .withOption() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/RequiredValueCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/RequiredValueCommands.java index eeae60a4..f06014f3 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/RequiredValueCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/RequiredValueCommands.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.samples.e2e; +import java.util.function.Supplier; + import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.standard.ShellComponent; @@ -37,8 +39,8 @@ public class RequiredValueCommands extends BaseE2ECommands { } @Bean - public CommandRegistration testRequiredValueRegistration() { - return CommandRegistration.builder() + public CommandRegistration testRequiredValueRegistration(Supplier builder) { + return builder.get() .command(REG, "required-value") .group(GROUP) .withOption() diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ValidatedValueCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ValidatedValueCommands.java index 6e2fbfa2..a068c916 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ValidatedValueCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/ValidatedValueCommands.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.samples.e2e; +import java.util.function.Supplier; + import jakarta.validation.constraints.Min; import org.springframework.context.annotation.Bean; @@ -40,8 +42,8 @@ public class ValidatedValueCommands extends BaseE2ECommands { } @Bean - public CommandRegistration testValidatedValueRegistration() { - return CommandRegistration.builder() + public CommandRegistration testValidatedValueRegistration(Supplier builder) { + return builder.get() .command(REG, "validated-value") .group(GROUP) .withOption() diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java index 2608e28e..49e04c61 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java @@ -30,7 +30,6 @@ 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; @@ -62,14 +61,16 @@ import org.springframework.util.StringUtils; * @author Camilo Gonzalez * @author Janne Valkealahti */ -public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, ApplicationContextAware { +public class StandardMethodTargetRegistrar implements MethodTargetRegistrar { private final Logger log = LoggerFactory.getLogger(StandardMethodTargetRegistrar.class); private ApplicationContext applicationContext; + private Supplier commandRegistrationBuilderSupplier; - @Override - public void setApplicationContext(ApplicationContext applicationContext) { + public StandardMethodTargetRegistrar(ApplicationContext applicationContext, + Supplier commandRegistrationBuilderSupplier) { this.applicationContext = applicationContext; + this.commandRegistrationBuilderSupplier = commandRegistrationBuilderSupplier; } @Override @@ -90,7 +91,7 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App log.debug("Registering with keys='{}' key='{}'", keys, key); Supplier availabilityIndicator = findAvailabilityIndicator(keys, bean, method); - Builder builder = CommandRegistration.builder() + Builder builder = commandRegistrationBuilderSupplier.get() .command(key) .group(group) .description(shellMapping.value()) @@ -210,7 +211,6 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar, App } builder.withTarget().method(bean, method); - CommandRegistration registration = builder.build(); registry.register(registration); }, method -> method.getAnnotation(ShellMethod.class) != null); diff --git a/spring-shell-standard/src/test/java/org/springframework/shell/standard/StandardMethodTargetRegistrarTests.java b/spring-shell-standard/src/test/java/org/springframework/shell/standard/StandardMethodTargetRegistrarTests.java index bb01e02d..211ac9f4 100644 --- a/spring-shell-standard/src/test/java/org/springframework/shell/standard/StandardMethodTargetRegistrarTests.java +++ b/spring-shell-standard/src/test/java/org/springframework/shell/standard/StandardMethodTargetRegistrarTests.java @@ -17,6 +17,7 @@ package org.springframework.shell.standard; import java.util.Map; +import java.util.function.Supplier; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,10 +43,11 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; */ public class StandardMethodTargetRegistrarTests { - private StandardMethodTargetRegistrar registrar = new StandardMethodTargetRegistrar(); + private StandardMethodTargetRegistrar registrar; private AnnotationConfigApplicationContext applicationContext; private CommandCatalog catalog; private DefaultShellContext shellContext; + private Supplier builder = () -> CommandRegistration.builder(); @BeforeEach public void setup() { @@ -65,7 +67,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testRegistrations() { applicationContext = new AnnotationConfigApplicationContext(Sample1.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); Map registrations = catalog.getRegistrations(); assertThat(registrations).hasSize(3); @@ -100,7 +102,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testOptionRequiredWithAnnotation() { applicationContext = new AnnotationConfigApplicationContext(Sample2.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); Map registrations = catalog.getRegistrations(); assertThat(registrations).hasSize(1); @@ -122,7 +124,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testOptionOptionalWithAnnotation() { applicationContext = new AnnotationConfigApplicationContext(Sample3.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); Map registrations = catalog.getRegistrations(); assertThat(registrations).hasSize(1); @@ -146,7 +148,7 @@ public class StandardMethodTargetRegistrarTests { public void testAvailabilityIndicators() { applicationContext = new AnnotationConfigApplicationContext(SampleWithAvailability.class); SampleWithAvailability sample = applicationContext.getBean(SampleWithAvailability.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); Map registrations = catalog.getRegistrations(); @@ -219,7 +221,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testAvailabilityIndicatorErrorMultipleExplicit() { applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorOnShellMethod.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); assertThatThrownBy(() -> { registrar.register(catalog); @@ -241,7 +243,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testAvailabilityIndicatorWildcardNotAlone() { applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorWildcardNotAlone.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); assertThatThrownBy(() -> { registrar.register(catalog); @@ -267,7 +269,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testAvailabilityIndicatorAmbiguous() { applicationContext = new AnnotationConfigApplicationContext(WrongAvailabilityIndicatorAmbiguous.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); assertThatThrownBy(() -> { registrar.register(catalog); @@ -301,7 +303,7 @@ public class StandardMethodTargetRegistrarTests { public void testGrouping() { applicationContext = new AnnotationConfigApplicationContext(GroupOneCommands.class, GroupTwoCommands.class, GroupThreeCommands.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); assertThat(catalog.getRegistrations().get("explicit1")).satisfies(registration -> { @@ -334,7 +336,7 @@ public class StandardMethodTargetRegistrarTests { public void testInteractionModeInteractive() { shellContext.setInteractionMode(InteractionMode.INTERACTIVE); applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); assertThat(catalog.getRegistrations().get("foo1")).isNotNull(); @@ -346,7 +348,7 @@ public class StandardMethodTargetRegistrarTests { public void testInteractionModeNonInteractive() { shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE); applicationContext = new AnnotationConfigApplicationContext(InteractionModeCommands.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); assertThat(catalog.getRegistrations().get("foo1")).isNull(); @@ -374,7 +376,7 @@ public class StandardMethodTargetRegistrarTests { public void testOptionUseDefaultValue() { shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE); applicationContext = new AnnotationConfigApplicationContext(DefaultValuesCommands.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); assertThat(catalog.getRegistrations().get("foo1")).isNotNull(); @@ -409,7 +411,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testOptionValuesWithBoolean() { applicationContext = new AnnotationConfigApplicationContext(ValuesWithBoolean.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); assertThat(catalog.getRegistrations().get("foo1")).isNotNull(); @@ -447,7 +449,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testOptionWithoutHyphenRegisterFromDefaultPrefix() { applicationContext = new AnnotationConfigApplicationContext(OptionWithoutHyphenRegisterFromDefaultPrefix.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); assertThat(catalog.getRegistrations().get("foo1")).isNotNull(); @@ -467,7 +469,7 @@ public class StandardMethodTargetRegistrarTests { @Test public void testOptionWithoutHyphenRegisterFromChangedPrefix() { applicationContext = new AnnotationConfigApplicationContext(OptionWithoutHyphenRegisterFromChangedPrefix.class); - registrar.setApplicationContext(applicationContext); + registrar = new StandardMethodTargetRegistrar(applicationContext, builder); registrar.register(catalog); assertThat(catalog.getRegistrations().get("foo1")).isNotNull();