From 39c01fe00ba8b5000cd222517c7cef5f97cd2ef1 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Tue, 14 Jun 2022 09:56:17 +0100 Subject: [PATCH] Fix availability feature - Bring back some missing functionality which got missing during the rework to new command model. - Polish some classes. - Restore origin sample. - Add availability things into help templates and its representation model. - Fixes #423 --- .../shell/CommandNotCurrentlyAvailable.java | 30 +++++------ .../shell/command/CommandExecution.java | 7 +++ .../shell/command/CommandExecutionTests.java | 20 ++++++++ .../command/CommandRegistrationTests.java | 24 +++++++++ .../samples/standard/DynamicCommands.java | 50 ++++++++++-------- .../CommandAvailabilityInfoModel.java | 51 +++++++++++++++++++ .../standard/commands/CommandInfoModel.java | 20 +++++++- .../standard/commands/GroupsInfoModel.java | 21 +++++++- .../template/help-command-default.stg | 10 ++++ .../template/help-commands-default.stg | 16 +++++- 10 files changed, 208 insertions(+), 41 deletions(-) create mode 100644 spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandAvailabilityInfoModel.java diff --git a/spring-shell-core/src/main/java/org/springframework/shell/CommandNotCurrentlyAvailable.java b/spring-shell-core/src/main/java/org/springframework/shell/CommandNotCurrentlyAvailable.java index 97edb0ce..11df2d3c 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/CommandNotCurrentlyAvailable.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/CommandNotCurrentlyAvailable.java @@ -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; /** @@ -23,20 +22,21 @@ package org.springframework.shell; */ public class CommandNotCurrentlyAvailable extends RuntimeException { - private final String command; - private final Availability availability; + private final String command; + private final Availability availability; - public CommandNotCurrentlyAvailable(String command, Availability availability) { - super(String.format("Command '%s' exists but is not currently available because %s", command, availability.getReason())); - this.command = command; - this.availability = availability; - } + public CommandNotCurrentlyAvailable(String command, Availability availability) { + super(String.format("Command '%s' exists but is not currently available because %s", command, + availability.getReason())); + this.command = command; + this.availability = availability; + } - public String getCommand() { - return command; - } + public String getCommand() { + return command; + } - public Availability getAvailability() { - return availability; - } + public Availability getAvailability() { + return availability; + } } 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 6aa0072c..64f9b83d 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 @@ -29,6 +29,8 @@ import org.springframework.core.convert.ConversionService; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.support.MessageBuilder; +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.TargetInfo; @@ -95,6 +97,11 @@ public interface CommandExecution { } public Object evaluate(CommandRegistration registration, String[] args) { + // fast fail with availability before doing anything else + Availability availability = registration.getAvailability(); + if (availability != null && !availability.isAvailable()) { + return new CommandNotCurrentlyAvailable(registration.getCommand(), availability); + } List options = registration.getOptions(); CommandParser parser = CommandParser.of(conversionService); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java index 0bbc0d37..1ef21d22 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java @@ -26,6 +26,8 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.shell.Availability; +import org.springframework.shell.CommandNotCurrentlyAvailable; import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException; import org.springframework.shell.command.CommandRegistration.OptionArity; @@ -459,4 +461,22 @@ public class CommandExecutionTests extends AbstractCommandTests { execution.evaluate(r1, new String[]{}); }).isInstanceOf(CommandParserExceptionsException.class); } + + @Test + public void testCommandNotAvailable() { + CommandRegistration r1 = CommandRegistration.builder() + .command("command1") + .description("help") + .withOption() + .longNames("arg1") + .description("some arg1") + .and() + .availability(() -> Availability.unavailable("fake reason")) + .withTarget() + .function(function1) + .and() + .build(); + Object result = execution.evaluate(r1, new String[]{"--arg1", "myarg1value"}); + assertThat(result).isInstanceOf(CommandNotCurrentlyAvailable.class); + } } 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 58788123..6979b21e 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 @@ -18,6 +18,7 @@ package org.springframework.shell.command; import org.junit.jupiter.api.Test; import org.springframework.core.ResolvableType; +import org.springframework.shell.Availability; import org.springframework.shell.command.CommandRegistration.OptionArity; import org.springframework.shell.command.CommandRegistration.TargetInfo.TargetType; import org.springframework.shell.context.InteractionMode; @@ -410,4 +411,27 @@ public class CommandRegistrationTests extends AbstractCommandTests { assertThat(registration.getExitCode()).isNotNull(); assertThat(registration.getExitCode().getMappingFunctions()).hasSize(4); } + + @Test + public void testAvailability() { + CommandRegistration registration; + registration = CommandRegistration.builder() + .command("command1") + .withTarget() + .function(function1) + .and() + .build(); + assertThat(registration.getAvailability()).isNotNull(); + assertThat(registration.getAvailability().isAvailable()).isTrue(); + + registration = CommandRegistration.builder() + .command("command1") + .availability(() -> Availability.unavailable("fake")) + .withTarget() + .function(function1) + .and() + .build(); + assertThat(registration.getAvailability()).isNotNull(); + assertThat(registration.getAvailability().isAvailable()).isFalse(); + } } diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/DynamicCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/DynamicCommands.java index 494210d6..3d1f6aa8 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/DynamicCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/DynamicCommands.java @@ -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. @@ -29,29 +29,37 @@ import org.springframework.shell.standard.ShellMethodAvailability; @ShellComponent public class DynamicCommands { - private boolean connected; + private boolean connected; - public Availability authenticateAvailability() { - return connected ? Availability.available() : Availability.unavailable("you are not connected"); - } + private boolean authenticated; - @ShellMethod(value = "Authenticate with the system", group = "Dynamic Commands") - public void authenticate(String credentials) { - } + public Availability authenticateAvailability() { + return connected ? Availability.available() : Availability.unavailable("you are not connected"); + } - @ShellMethod(value = "Connect to the system", group = "Dynamic Commands") - public void connect() { - connected = true; - } + @ShellMethod(value = "Authenticate with the system", group = "Dynamic Commands") + public void authenticate(String credentials) { + authenticated = "sesame".equals(credentials); + } - @ShellMethod(value = "Disconnect from the system", group = "Dynamic Commands") - public void disconnect() { - connected = false; - } + @ShellMethod(value = "Connect to the system", group = "Dynamic Commands") + public void connect() { + connected = true; + } - @ShellMethod(value = "Blow Everything up", group = "Dynamic Commands") - @ShellMethodAvailability("dangerousAvailability") - public String blowUp() { - return "Boom!"; - } + @ShellMethod(value = "Disconnect from the system", group = "Dynamic Commands") + public void disconnect() { + connected = false; + } + + @ShellMethod(value = "Blow Everything up", group = "Dynamic Commands") + @ShellMethodAvailability("dangerousAvailability") + public String blowUp() { + return "Boom!"; + } + + public Availability dangerousAvailability() { + return connected && authenticated ? Availability.available() + : Availability.unavailable("you failed to authenticate. Try 'sesame'."); + } } diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandAvailabilityInfoModel.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandAvailabilityInfoModel.java new file mode 100644 index 00000000..79a94985 --- /dev/null +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandAvailabilityInfoModel.java @@ -0,0 +1,51 @@ +/* + * 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.commands; + +/** + * Model encapsulating info about {@code command availability}. + * + * @author Janne Valkealahti + */ +class CommandAvailabilityInfoModel { + + private boolean available; + private String reason; + + CommandAvailabilityInfoModel(boolean available, String reason) { + this.available = available; + this.reason = reason; + } + + /** + * Builds {@link CommandAvailabilityInfoModel}. + * + * @param available the available flag + * @param reason the reason + * @return a command parameter availability model + */ + static CommandAvailabilityInfoModel of(boolean available, String reason) { + return new CommandAvailabilityInfoModel(available, reason); + } + + public boolean getAvailable() { + return available; + } + + public String getReason() { + return reason; + } +} diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java index c6e27737..a611eae1 100644 --- a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.springframework.shell.Availability; import org.springframework.shell.command.CommandOption; import org.springframework.shell.command.CommandRegistration; import org.springframework.util.ClassUtils; @@ -34,11 +35,14 @@ class CommandInfoModel { private String name; private String description; private List parameters; + private CommandAvailabilityInfoModel availability; - CommandInfoModel(String name, String description, List parameters) { + CommandInfoModel(String name, String description, List parameters, + CommandAvailabilityInfoModel availability) { this.name = name; this.description = description; this.parameters = parameters; + this.availability = availability; } /** @@ -65,7 +69,15 @@ class CommandInfoModel { .collect(Collectors.toList()); String description = registration.getDescription(); - return new CommandInfoModel(name, description, parameters); + boolean available = true; + String availReason = ""; + if (registration.getAvailability() != null) { + Availability a = registration.getAvailability(); + available = a.isAvailable(); + availReason = a.getReason(); + } + CommandAvailabilityInfoModel availModel = CommandAvailabilityInfoModel.of(available, availReason); + return new CommandInfoModel(name, description, parameters, availModel); } private static String commandOptionType(CommandOption o) { @@ -88,4 +100,8 @@ class CommandInfoModel { public List getParameters() { return parameters; } + + public CommandAvailabilityInfoModel getAvailability() { + return availability; + } } diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupsInfoModel.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupsInfoModel.java index a4451b22..5af11b8e 100644 --- a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupsInfoModel.java +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/GroupsInfoModel.java @@ -36,11 +36,14 @@ class GroupsInfoModel { private boolean showGroups = true; private final List groups; private final List commands; + private boolean hasUnavailableCommands = false; - GroupsInfoModel(boolean showGroups, List groups, List commands) { + GroupsInfoModel(boolean showGroups, List groups, List commands, + boolean hasUnavailableCommands) { this.showGroups = showGroups; this.groups = groups; this.commands = commands; + this.hasUnavailableCommands = hasUnavailableCommands; } /** @@ -71,7 +74,17 @@ class GroupsInfoModel { List commands = gcims.stream() .flatMap(gcim -> gcim.getCommands().stream()) .collect(Collectors.toList()); - return new GroupsInfoModel(showGroups, gcims, commands); + boolean hasUnavailableCommands = commands.stream() + .map(c -> { + if (c.getAvailability() != null) { + return c.getAvailability().getAvailable(); + } + return true; + }) + .filter(a -> !a) + .findFirst() + .isPresent(); + return new GroupsInfoModel(showGroups, gcims, commands, hasUnavailableCommands); } public boolean getShowGroups() { @@ -85,4 +98,8 @@ class GroupsInfoModel { public List getCommands() { return commands; } + + public boolean getHasUnavailableCommands() { + return hasUnavailableCommands; + } } diff --git a/spring-shell-standard-commands/src/main/resources/template/help-command-default.stg b/spring-shell-standard-commands/src/main/resources/template/help-command-default.stg index e202d4f7..245c972c 100644 --- a/spring-shell-standard-commands/src/main/resources/template/help-command-default.stg +++ b/spring-shell-standard-commands/src/main/resources/template/help-command-default.stg @@ -57,6 +57,15 @@ options(options) ::= << }> >> +// AVAILABILITY +availability(availability) ::= << + + +<("CURRENTLY UNAVAILABLE"); format="style-highlight"> + + +>> + // main main(model) ::= << @@ -64,4 +73,5 @@ main(model) ::= << + >> diff --git a/spring-shell-standard-commands/src/main/resources/template/help-commands-default.stg b/spring-shell-standard-commands/src/main/resources/template/help-commands-default.stg index e9d5843e..430a00f5 100644 --- a/spring-shell-standard-commands/src/main/resources/template/help-commands-default.stg +++ b/spring-shell-standard-commands/src/main/resources/template/help-commands-default.stg @@ -3,8 +3,21 @@ name() ::= << >> +availability(availability) ::= <% + +<("* "); format="style-highlight"> + +%> + +availabilityDesc(hasUnavailableCommands) ::= << + +<("Commands marked with (*) are currently unavailable.")> +<("Type `help ` to learn more.")> + +>> + command(command) ::= << -<(command.name); format="style-highlight"><(":"); format="style-highlight"> +<(command.name); format="style-highlight"><(":"); format="style-highlight"> >> @@ -29,4 +42,5 @@ main(model) ::= << + >>