Skip to content

Commit 0efd71a

Browse files
authored
Add non-interactive shell runner customizer
- Add non-interactive shell runner customizer - Update javadoc for DefaultApplicationRunner - Rename DefaultApplicationRunner - Formatting
1 parent 9d7a345 commit 0efd71a

File tree

8 files changed

+174
-41
lines changed

8 files changed

+174
-41
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2222
import org.springframework.context.annotation.Bean;
2323
import org.springframework.context.annotation.Configuration;
24-
import org.springframework.shell.DefaultApplicationRunner;
24+
import org.springframework.shell.DefaultShellApplicationRunner;
2525
import org.springframework.shell.ShellApplicationRunner;
2626
import org.springframework.shell.ShellRunner;
2727

@@ -31,7 +31,7 @@ public class ApplicationRunnerAutoConfiguration {
3131

3232
@Bean
3333
@ConditionalOnMissingBean(ShellApplicationRunner.class)
34-
public DefaultApplicationRunner defaultApplicationRunner(List<ShellRunner> shellRunners) {
35-
return new DefaultApplicationRunner(shellRunners);
34+
public DefaultShellApplicationRunner defaultShellApplicationRunner(List<ShellRunner> shellRunners) {
35+
return new DefaultShellApplicationRunner(shellRunners);
3636
}
3737
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.springframework.shell.boot;
2+
3+
import org.springframework.shell.jline.NonInteractiveShellRunner;
4+
5+
/**
6+
* Callback interface that can be implemented by beans wishing to customize the
7+
* auto-configured {@link NonInteractiveShellRunner}.
8+
*
9+
* @author Chris Bono
10+
* @since 2.1.0
11+
*/
12+
@FunctionalInterface
13+
public interface NonInteractiveShellRunnerCustomizer {
14+
/**
15+
* Customize the {@link NonInteractiveShellRunner}.
16+
* @param shellRunner the non-interactive shell runner to customize
17+
*/
18+
void customize(NonInteractiveShellRunner shellRunner);
19+
20+
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.jline.reader.LineReader;
1919
import org.jline.reader.Parser;
2020

21+
import org.springframework.beans.factory.ObjectProvider;
2122
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2223
import org.springframework.context.annotation.Bean;
2324
import org.springframework.context.annotation.Configuration;
@@ -54,8 +55,10 @@ public InteractiveShellRunner interactiveApplicationRunner() {
5455

5556
@Bean
5657
@ConditionalOnProperty(prefix = "spring.shell.noninteractive", value = "enabled", havingValue = "true", matchIfMissing = true)
57-
public NonInteractiveShellRunner nonInteractiveApplicationRunner() {
58-
return new NonInteractiveShellRunner(shell, shellContext);
58+
public NonInteractiveShellRunner nonInteractiveApplicationRunner(ObjectProvider<NonInteractiveShellRunnerCustomizer> customizer) {
59+
NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext);
60+
customizer.orderedStream().forEach((c) -> c.customize(shellRunner));
61+
return shellRunner;
5962
}
6063

6164
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.boot;
17+
18+
import org.jline.reader.LineReader;
19+
import org.jline.reader.Parser;
20+
import org.junit.jupiter.api.Nested;
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.boot.autoconfigure.AutoConfigurations;
24+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
25+
import org.springframework.shell.ParameterResolver;
26+
import org.springframework.shell.Shell;
27+
import org.springframework.shell.context.ShellContext;
28+
import org.springframework.shell.jline.InteractiveShellRunner;
29+
import org.springframework.shell.jline.NonInteractiveShellRunner;
30+
import org.springframework.shell.jline.PromptProvider;
31+
import org.springframework.shell.jline.ScriptShellRunner;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.verify;
36+
37+
/**
38+
* Tests for {@link ShellRunnerAutoConfiguration}.
39+
*/
40+
class ShellRunnerAutoConfigurationTests {
41+
42+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
43+
.withConfiguration(AutoConfigurations.of(ShellRunnerAutoConfiguration.class))
44+
.withBean(Shell.class, () -> mock(Shell.class))
45+
.withBean(PromptProvider.class, () -> mock(PromptProvider.class))
46+
.withBean(LineReader.class, () -> mock(LineReader.class))
47+
.withBean(Parser.class, () -> mock(Parser.class))
48+
.withBean(ShellContext.class, () -> mock(ShellContext.class))
49+
.withBean(ParameterResolver.class, () -> mock(ParameterResolver.class));
50+
51+
@Nested
52+
class Interactive {
53+
@Test
54+
void enabledByDefault() {
55+
contextRunner.run(context -> assertThat(context).hasSingleBean(InteractiveShellRunner.class));
56+
}
57+
58+
@Test
59+
void disabledWhenPropertySet() {
60+
contextRunner.withPropertyValues("spring.shell.interactive.enabled:false")
61+
.run(context -> assertThat(context).doesNotHaveBean(InteractiveShellRunner.class));
62+
}
63+
}
64+
65+
@Nested
66+
class NonInteractive {
67+
@Test
68+
void enabledByDefault() {
69+
contextRunner.run(context -> assertThat(context).hasSingleBean(NonInteractiveShellRunner.class));
70+
}
71+
72+
@Test
73+
void disabledWhenPropertySet() {
74+
contextRunner.withPropertyValues("spring.shell.noninteractive.enabled:false")
75+
.run(context -> assertThat(context).doesNotHaveBean(NonInteractiveShellRunner.class));
76+
}
77+
78+
@Test
79+
void canBeCustomized() {
80+
NonInteractiveShellRunnerCustomizer customizer = mock(NonInteractiveShellRunnerCustomizer.class);
81+
contextRunner.withBean(NonInteractiveShellRunnerCustomizer.class, () -> customizer)
82+
.run(context -> {
83+
NonInteractiveShellRunner runner = context.getBean(NonInteractiveShellRunner.class);
84+
verify(customizer).customize(runner);
85+
});
86+
}
87+
}
88+
89+
@Nested
90+
class Script {
91+
@Test
92+
void enabledByDefault() {
93+
contextRunner.run(context -> assertThat(context).hasSingleBean(ScriptShellRunner.class));
94+
}
95+
96+
@Test
97+
void disabledWhenPropertySet() {
98+
contextRunner.withPropertyValues("spring.shell.script.enabled:false")
99+
.run(context -> assertThat(context).doesNotHaveBean(ScriptShellRunner.class));
100+
}
101+
}
102+
}

spring-shell-core/src/main/java/org/springframework/shell/DefaultApplicationRunner.java renamed to spring-shell-core/src/main/java/org/springframework/shell/DefaultShellApplicationRunner.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 the original author or authors.
2+
* Copyright 2021-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,21 +23,28 @@
2323
import org.slf4j.LoggerFactory;
2424

2525
import org.springframework.boot.ApplicationArguments;
26-
import org.springframework.boot.ApplicationRunner;
2726
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
27+
import org.springframework.core.annotation.Order;
2828

2929
/**
30-
* Default {@link ApplicationRunner} which dispatches to first ordered
31-
* {@link ShellRunner} able to handle shell.
30+
* Default {@link ShellApplicationRunner} which dispatches to the first ordered {@link ShellRunner} able to handle
31+
* the shell.
3232
*
3333
* @author Janne Valkealahti
34+
* @author Chris Bono
3435
*/
35-
public class DefaultApplicationRunner implements ShellApplicationRunner {
36+
@Order(DefaultShellApplicationRunner.PRECEDENCE)
37+
public class DefaultShellApplicationRunner implements ShellApplicationRunner {
3638

37-
private final static Logger log = LoggerFactory.getLogger(DefaultApplicationRunner.class);
39+
/**
40+
* The precedence at which this runner is executed with respect to other ApplicationRunner beans
41+
*/
42+
public static final int PRECEDENCE = 0;
43+
44+
private final static Logger log = LoggerFactory.getLogger(DefaultShellApplicationRunner.class);
3845
private final List<ShellRunner> shellRunners;
3946

40-
public DefaultApplicationRunner(List<ShellRunner> shellRunners) {
47+
public DefaultShellApplicationRunner(List<ShellRunner> shellRunners) {
4148
// TODO: follow up with spring-native
4249
// Looks like with fatjar it comes on a correct order from
4350
// a context(not really sure if that's how spring context works) but

spring-shell-core/src/main/java/org/springframework/shell/jline/InteractiveShellRunner.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017 the original author or authors.
2+
* Copyright 2017-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,20 +31,21 @@
3131
import org.springframework.shell.context.ShellContext;
3232

3333
/**
34-
* Default Boot runner that bootstraps the shell application in interactive
35-
* mode.
34+
* A {@link ShellRunner} that bootstraps the shell in interactive mode.
3635
*
37-
* Runs the REPL of the shell unless the {@literal spring.shell.interactive}
38-
* property has been set to {@literal false}.
36+
* <p>Has lower precedence than {@link ScriptShellRunner} and {@link NonInteractiveShellRunner} which makes it the
37+
* default shell runner when the other runners opt-out of handling the shell.
3938
*
4039
* @author Eric Bottard
40+
* @author Janne Valkealahti
41+
* @author Chris Bono
4142
*/
4243
@Order(InteractiveShellRunner.PRECEDENCE)
4344
public class InteractiveShellRunner implements ShellRunner {
4445

4546
/**
46-
* The precedence at which this runner is set. Highger precedence runners may effectively disable this one by setting
47-
* the {@link #SPRING_SHELL_INTERACTIVE_ENABLED} property to {@literal false}.
47+
* The precedence at which this runner is ordered by the DefaultApplicationRunner - which also controls
48+
* the order it is consulted on the ability to handle the current shell.
4849
*/
4950
public static final int PRECEDENCE = 0;
5051

spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java

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

1818
import java.util.Arrays;
1919
import java.util.List;
20+
import java.util.function.Function;
2021

2122
import org.springframework.boot.ApplicationArguments;
2223
import org.springframework.core.annotation.Order;
@@ -26,37 +27,45 @@
2627
import org.springframework.shell.ShellRunner;
2728
import org.springframework.shell.context.InteractionMode;
2829
import org.springframework.shell.context.ShellContext;
29-
import org.springframework.util.ObjectUtils;
3030
import org.springframework.util.StringUtils;
3131

3232
/**
33-
* Non interactive {@link ShellRunner} which is meant to execute shell commands
34-
* without entering interactive shell.
33+
* A {@link ShellRunner} that executes commands without entering interactive shell mode.
34+
*
35+
* <p>Has higher precedence than {@link InteractiveShellRunner} which gives it an opportunity to handle the shell
36+
* in non-interactive fashion.
3537
*
3638
* @author Janne Valkealahti
39+
* @author Chris Bono
3740
*/
3841
@Order(InteractiveShellRunner.PRECEDENCE - 50)
3942
public class NonInteractiveShellRunner implements ShellRunner {
4043

4144
private final Shell shell;
45+
4246
private final ShellContext shellContext;
4347

48+
private Function<ApplicationArguments, List<String>> argsToShellCommand = (args) -> Arrays.asList(args.getSourceArgs());
49+
4450
public NonInteractiveShellRunner(Shell shell, ShellContext shellContext) {
4551
this.shell = shell;
4652
this.shellContext = shellContext;
4753
}
4854

55+
public void setArgsToShellCommand(Function<ApplicationArguments, List<String>> argsToShellCommand) {
56+
this.argsToShellCommand = argsToShellCommand;
57+
}
58+
4959
@Override
5060
public boolean canRun(ApplicationArguments args) {
51-
List<String> argsToShellCommand = Arrays.asList(args.getSourceArgs());
52-
return !ObjectUtils.isEmpty(argsToShellCommand);
61+
return !argsToShellCommand.apply(args).isEmpty();
5362
}
5463

5564
@Override
5665
public void run(ApplicationArguments args) throws Exception {
5766
shellContext.setInteractionMode(InteractionMode.NONINTERACTIVE);
58-
List<String> argsToShellCommand = Arrays.asList(args.getSourceArgs());
59-
InputProvider inputProvider = new StringInputProvider(argsToShellCommand);
67+
List<String> commands = this.argsToShellCommand.apply(args);
68+
InputProvider inputProvider = new StringInputProvider(commands);
6069
shell.run(inputProvider);
6170
}
6271

spring-shell-core/src/main/java/org/springframework/shell/jline/ScriptShellRunner.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2018 the original author or authors.
2+
* Copyright 2018-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,11 +31,11 @@
3131
import org.springframework.util.ObjectUtils;
3232

3333
/**
34-
* Spring Boot ApplicationRunner that looks for process arguments that start with
35-
* {@literal @}, which are then interpreted as references to script files to run and exit.
34+
* A {@link ShellRunner} that looks for process arguments that start with {@literal @}, which are then interpreted as
35+
* references to script files to run and exit.
3636
*
37-
* Has higher precedence than {@link InteractiveShellRunner} so that it
38-
* prevents it to run if scripts are found.
37+
* <p>Has higher precedence than {@link NonInteractiveShellRunner} and {@link InteractiveShellRunner} which gives it
38+
* top priority to run the shell if scripts are found.
3939
*
4040
* @author Eric Bottard
4141
*/
@@ -44,15 +44,6 @@
4444
public class ScriptShellRunner implements ShellRunner {
4545
//end::documentation[]
4646

47-
public static final String SPRING_SHELL_SCRIPT = "spring.shell.script";
48-
public static final String ENABLED = "enabled";
49-
50-
/**
51-
* The name of the environment property that allows to disable the behavior of this
52-
* runner.
53-
*/
54-
public static final String SPRING_SHELL_SCRIPT_ENABLED = SPRING_SHELL_SCRIPT + "." + ENABLED;
55-
5647
private final Parser parser;
5748

5849
private final Shell shell;

0 commit comments

Comments
 (0)