Skip to content

Add non-interactive shell runner customizer #358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,7 +31,7 @@ public class ApplicationRunnerAutoConfiguration {

@Bean
@ConditionalOnMissingBean(ShellApplicationRunner.class)
public DefaultApplicationRunner defaultApplicationRunner(List<ShellRunner> shellRunners) {
return new DefaultApplicationRunner(shellRunners);
public DefaultShellApplicationRunner defaultShellApplicationRunner(List<ShellRunner> shellRunners) {
return new DefaultShellApplicationRunner(shellRunners);
}
}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<NonInteractiveShellRunnerCustomizer> customizer) {
NonInteractiveShellRunner shellRunner = new NonInteractiveShellRunner(shell, shellContext);
customizer.orderedStream().forEach((c) -> c.customize(shellRunner));
return shellRunner;
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<ShellRunner> shellRunners;

public DefaultApplicationRunner(List<ShellRunner> shellRunners) {
public DefaultShellApplicationRunner(List<ShellRunner> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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}.
* <p>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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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.
*
* <p>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<ApplicationArguments, List<String>> argsToShellCommand = (args) -> Arrays.asList(args.getSourceArgs());

public NonInteractiveShellRunner(Shell shell, ShellContext shellContext) {
this.shell = shell;
this.shellContext = shellContext;
}

public void setArgsToShellCommand(Function<ApplicationArguments, List<String>> argsToShellCommand) {
this.argsToShellCommand = argsToShellCommand;
}

@Override
public boolean canRun(ApplicationArguments args) {
List<String> 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<String> argsToShellCommand = Arrays.asList(args.getSourceArgs());
InputProvider inputProvider = new StringInputProvider(argsToShellCommand);
List<String> commands = this.argsToShellCommand.apply(args);
InputProvider inputProvider = new StringInputProvider(commands);
shell.run(inputProvider);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
* <p>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
*/
Expand All @@ -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;
Expand Down