diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ApplicationRunnerAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ApplicationRunnerAutoConfiguration.java index e14d268af..07cd8792e 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ApplicationRunnerAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ApplicationRunnerAutoConfiguration.java @@ -21,7 +21,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.shell.DefaultApplicationRunner; +import org.springframework.shell.DefaultShellApplicationRunner; import org.springframework.shell.ShellApplicationRunner; import org.springframework.shell.ShellRunner; @@ -31,7 +31,7 @@ public class ApplicationRunnerAutoConfiguration { @Bean @ConditionalOnMissingBean(ShellApplicationRunner.class) - public DefaultApplicationRunner defaultApplicationRunner(List shellRunners) { - return new DefaultApplicationRunner(shellRunners); + public DefaultShellApplicationRunner defaultShellApplicationRunner(List shellRunners) { + return new DefaultShellApplicationRunner(shellRunners); } } diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/NonInteractiveShellRunnerCustomizer.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/NonInteractiveShellRunnerCustomizer.java new file mode 100644 index 000000000..23ee3a1d1 --- /dev/null +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/NonInteractiveShellRunnerCustomizer.java @@ -0,0 +1,20 @@ +package org.springframework.shell.boot; + +import org.springframework.shell.jline.NonInteractiveShellRunner; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * auto-configured {@link NonInteractiveShellRunner}. + * + * @author Chris Bono + * @since 2.1.0 + */ +@FunctionalInterface +public interface NonInteractiveShellRunnerCustomizer { + /** + * Customize the {@link NonInteractiveShellRunner}. + * @param shellRunner the non-interactive shell runner to customize + */ + void customize(NonInteractiveShellRunner shellRunner); + +} diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ShellRunnerAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ShellRunnerAutoConfiguration.java index ff11d62fb..e8242adac 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ShellRunnerAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ShellRunnerAutoConfiguration.java @@ -18,6 +18,7 @@ import org.jline.reader.LineReader; import org.jline.reader.Parser; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,8 +55,10 @@ public InteractiveShellRunner interactiveApplicationRunner() { @Bean @ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "enabled", havingValue = "true", matchIfMissing = true) - public NonInteractiveShellRunner nonInteractiveApplicationRunner() { - return new NonInteractiveShellRunner(shell, shellContext); + public NonInteractiveShellRunner nonInteractiveApplicationRunner(ObjectProvider customizer) { + NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext); + customizer.orderedStream().forEach((c) -> c.customize(shellRunner)); + return shellRunner; } @Bean diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/ShellRunnerAutoConfigurationTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/ShellRunnerAutoConfigurationTests.java new file mode 100644 index 000000000..009b4c555 --- /dev/null +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/ShellRunnerAutoConfigurationTests.java @@ -0,0 +1,102 @@ +/* + * 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.jline.reader.LineReader; +import org.jline.reader.Parser; +import org.junit.jupiter.api.Nested; +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.context.ShellContext; +import org.springframework.shell.jline.InteractiveShellRunner; +import org.springframework.shell.jline.NonInteractiveShellRunner; +import org.springframework.shell.jline.PromptProvider; +import org.springframework.shell.jline.ScriptShellRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ShellRunnerAutoConfiguration}. + */ +class ShellRunnerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ShellRunnerAutoConfiguration.class)) + .withBean(Shell.class, () -> mock(Shell.class)) + .withBean(PromptProvider.class, () -> mock(PromptProvider.class)) + .withBean(LineReader.class, () -> mock(LineReader.class)) + .withBean(Parser.class, () -> mock(Parser.class)) + .withBean(ShellContext.class, () -> mock(ShellContext.class)) + .withBean(ParameterResolver.class, () -> mock(ParameterResolver.class)); + + @Nested + class Interactive { + @Test + void enabledByDefault() { + contextRunner.run(context -> assertThat(context).hasSingleBean(InteractiveShellRunner.class)); + } + + @Test + void disabledWhenPropertySet() { + contextRunner.withPropertyValues("spring.shell.interactive.enabled:false") + .run(context -> assertThat(context).doesNotHaveBean(InteractiveShellRunner.class)); + } + } + + @Nested + class NonInteractive { + @Test + void enabledByDefault() { + contextRunner.run(context -> assertThat(context).hasSingleBean(NonInteractiveShellRunner.class)); + } + + @Test + void disabledWhenPropertySet() { + contextRunner.withPropertyValues("spring.shell.noninteractive.enabled:false") + .run(context -> assertThat(context).doesNotHaveBean(NonInteractiveShellRunner.class)); + } + + @Test + void canBeCustomized() { + NonInteractiveShellRunnerCustomizer customizer = mock(NonInteractiveShellRunnerCustomizer.class); + contextRunner.withBean(NonInteractiveShellRunnerCustomizer.class, () -> customizer) + .run(context -> { + NonInteractiveShellRunner runner = context.getBean(NonInteractiveShellRunner.class); + verify(customizer).customize(runner); + }); + } + } + + @Nested + class Script { + @Test + void enabledByDefault() { + contextRunner.run(context -> assertThat(context).hasSingleBean(ScriptShellRunner.class)); + } + + @Test + void disabledWhenPropertySet() { + contextRunner.withPropertyValues("spring.shell.script.enabled:false") + .run(context -> assertThat(context).doesNotHaveBean(ScriptShellRunner.class)); + } + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/DefaultApplicationRunner.java b/spring-shell-core/src/main/java/org/springframework/shell/DefaultShellApplicationRunner.java similarity index 73% rename from spring-shell-core/src/main/java/org/springframework/shell/DefaultApplicationRunner.java rename to spring-shell-core/src/main/java/org/springframework/shell/DefaultShellApplicationRunner.java index 48d2bd424..e2d422590 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/DefaultApplicationRunner.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/DefaultShellApplicationRunner.java @@ -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. @@ -23,21 +23,28 @@ import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; /** - * Default {@link ApplicationRunner} which dispatches to first ordered - * {@link ShellRunner} able to handle shell. + * Default {@link ShellApplicationRunner} which dispatches to the first ordered {@link ShellRunner} able to handle + * the shell. * * @author Janne Valkealahti + * @author Chris Bono */ -public class DefaultApplicationRunner implements ShellApplicationRunner { +@Order(DefaultShellApplicationRunner.PRECEDENCE) +public class DefaultShellApplicationRunner implements ShellApplicationRunner { - private final static Logger log = LoggerFactory.getLogger(DefaultApplicationRunner.class); + /** + * The precedence at which this runner is executed with respect to other ApplicationRunner beans + */ + public static final int PRECEDENCE = 0; + + private final static Logger log = LoggerFactory.getLogger(DefaultShellApplicationRunner.class); private final List shellRunners; - public DefaultApplicationRunner(List shellRunners) { + public DefaultShellApplicationRunner(List shellRunners) { // TODO: follow up with spring-native // Looks like with fatjar it comes on a correct order from // a context(not really sure if that's how spring context works) but diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/InteractiveShellRunner.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/InteractiveShellRunner.java index a0fdfd073..c292f5a95 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/InteractiveShellRunner.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/InteractiveShellRunner.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. @@ -31,20 +31,21 @@ import org.springframework.shell.context.ShellContext; /** - * Default Boot runner that bootstraps the shell application in interactive - * mode. + * A {@link ShellRunner} that bootstraps the shell in interactive mode. * - * Runs the REPL of the shell unless the {@literal spring.shell.interactive} - * property has been set to {@literal false}. + *

Has lower precedence than {@link ScriptShellRunner} and {@link NonInteractiveShellRunner} which makes it the + * default shell runner when the other runners opt-out of handling the shell. * * @author Eric Bottard + * @author Janne Valkealahti + * @author Chris Bono */ @Order(InteractiveShellRunner.PRECEDENCE) public class InteractiveShellRunner implements ShellRunner { /** - * The precedence at which this runner is set. Highger precedence runners may effectively disable this one by setting - * the {@link #SPRING_SHELL_INTERACTIVE_ENABLED} property to {@literal false}. + * The precedence at which this runner is ordered by the DefaultApplicationRunner - which also controls + * the order it is consulted on the ability to handle the current shell. */ public static final int PRECEDENCE = 0; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java index 90c4ea428..7832a85f5 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java @@ -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. @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.List; +import java.util.function.Function; import org.springframework.boot.ApplicationArguments; import org.springframework.core.annotation.Order; @@ -26,37 +27,45 @@ import org.springframework.shell.ShellRunner; import org.springframework.shell.context.InteractionMode; import org.springframework.shell.context.ShellContext; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * Non interactive {@link ShellRunner} which is meant to execute shell commands - * without entering interactive shell. + * A {@link ShellRunner} that executes commands without entering interactive shell mode. + * + *

Has higher precedence than {@link InteractiveShellRunner} which gives it an opportunity to handle the shell + * in non-interactive fashion. * * @author Janne Valkealahti + * @author Chris Bono */ @Order(InteractiveShellRunner.PRECEDENCE - 50) public class NonInteractiveShellRunner implements ShellRunner { private final Shell shell; + private final ShellContext shellContext; + private Function> argsToShellCommand = (args) -> Arrays.asList(args.getSourceArgs()); + public NonInteractiveShellRunner(Shell shell, ShellContext shellContext) { this.shell = shell; this.shellContext = shellContext; } + public void setArgsToShellCommand(Function> argsToShellCommand) { + this.argsToShellCommand = argsToShellCommand; + } + @Override public boolean canRun(ApplicationArguments args) { - List argsToShellCommand = Arrays.asList(args.getSourceArgs()); - return !ObjectUtils.isEmpty(argsToShellCommand); + return !argsToShellCommand.apply(args).isEmpty(); } @Override public void run(ApplicationArguments args) throws Exception { shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE); - List argsToShellCommand = Arrays.asList(args.getSourceArgs()); - InputProvider inputProvider = new StringInputProvider(argsToShellCommand); + List commands = this.argsToShellCommand.apply(args); + InputProvider inputProvider = new StringInputProvider(commands); shell.run(inputProvider); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/ScriptShellRunner.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/ScriptShellRunner.java index 7bf57345c..ebdbdc8be 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/ScriptShellRunner.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/ScriptShellRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. @@ -31,11 +31,11 @@ import org.springframework.util.ObjectUtils; /** - * Spring Boot ApplicationRunner that looks for process arguments that start with - * {@literal @}, which are then interpreted as references to script files to run and exit. + * A {@link ShellRunner} that looks for process arguments that start with {@literal @}, which are then interpreted as + * references to script files to run and exit. * - * Has higher precedence than {@link InteractiveShellRunner} so that it - * prevents it to run if scripts are found. + *

Has higher precedence than {@link NonInteractiveShellRunner} and {@link InteractiveShellRunner} which gives it + * top priority to run the shell if scripts are found. * * @author Eric Bottard */ @@ -44,15 +44,6 @@ public class ScriptShellRunner implements ShellRunner { //end::documentation[] - public static final String SPRING_SHELL_SCRIPT = "spring.shell.script"; - public static final String ENABLED = "enabled"; - - /** - * The name of the environment property that allows to disable the behavior of this - * runner. - */ - public static final String SPRING_SHELL_SCRIPT_ENABLED = SPRING_SHELL_SCRIPT + "." + ENABLED; - private final Parser parser; private final Shell shell;