Extract autoconfig

- Create separate spring-shell-autoconfigure and keep
  all autoconfig features there.
- Fixes #329
This commit is contained in:
Janne Valkealahti
2021-12-19 12:31:22 +00:00
parent a2f3dd9ed8
commit 5dcdc4c185
21 changed files with 133 additions and 56 deletions

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2021 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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.shell.Shell;
import org.springframework.shell.jline.InteractiveShellApplicationRunner;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.shell.jline.ScriptShellApplicationRunner;
import static org.springframework.shell.jline.InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE;
import static org.springframework.shell.jline.ScriptShellApplicationRunner.SPRING_SHELL_SCRIPT;
@Configuration
public class ApplicationRunnerAutoConfiguration {
@Autowired
private Shell shell;
@Autowired
private PromptProvider promptProvider;
@Autowired
private LineReader lineReader;
@Bean
@ConditionalOnProperty(prefix = SPRING_SHELL_INTERACTIVE, value = InteractiveShellApplicationRunner.ENABLED, havingValue = "true", matchIfMissing = true)
public ApplicationRunner interactiveApplicationRunner(Environment environment) {
return new InteractiveShellApplicationRunner(lineReader, promptProvider, shell, environment);
}
@Bean
@ConditionalOnProperty(prefix = SPRING_SHELL_SCRIPT, value = ScriptShellApplicationRunner.ENABLED, havingValue = "true", matchIfMissing = true)
public ApplicationRunner scriptApplicationRunner(Parser parser, ConfigurableEnvironment environment) {
return new ScriptShellApplicationRunner(parser, shell, environment);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2021 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.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.CommandRegistry;
import org.springframework.shell.ConfigurableCommandRegistry;
import org.springframework.shell.MethodTargetRegistrar;
@Configuration(proxyBeanMethods = false)
public class CommandRegistryAutoConfiguration {
@Bean
public CommandRegistry commandRegistry(
ObjectProvider<MethodTargetRegistrar> methodTargerRegistrars) {
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry();
methodTargerRegistrars.orderedStream().forEach(resolver -> {
resolver.register(registry);
});
return registry;
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2021 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 java.util.List;
import java.util.stream.Collectors;
import org.jline.reader.Candidate;
import org.jline.reader.Completer;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.CompletingParsedLine;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.Shell;
@Configuration
public class CompleterAutoConfiguration {
@Autowired
private Shell shell;
@Bean
public CompleterAdapter completer() {
CompleterAdapter completerAdapter = new CompleterAdapter();
completerAdapter.setShell(shell);
return completerAdapter;
}
public static class CompleterAdapter implements Completer {
private Shell shell;
@Override
public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
CompletingParsedLine cpl = (line instanceof CompletingParsedLine) ? ((CompletingParsedLine) line) : t -> t;
CompletionContext context = new CompletionContext(sanitizeInput(line.words()), line.wordIndex(), line.wordCursor());
List<CompletionProposal> proposals = shell.complete(context);
proposals.stream()
.map(p -> new Candidate(
p.dontQuote() ? p.value() : cpl.emit(p.value()).toString(),
p.displayText(),
p.category(),
p.description(),
null,
null,
true)
)
.forEach(candidates::add);
}
public void setShell(Shell shell) {
this.shell = shell;
}
static List<String> sanitizeInput(List<String> words) {
words = words.stream()
.map(s -> s.replaceAll("^\\n+|\\n+$", "")) // CR at beginning/end of line introduced by backslash continuation
.map(s -> s.replaceAll("\\n+", " ")) // CR in middle of word introduced by return inside a quoted string
.collect(Collectors.toList());
return words;
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2017-2021 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 com.beust.jcommander.JCommander;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.jcommander.JCommanderParameterResolver;
import org.springframework.context.annotation.Bean;
/**
* Registers JCommanderParameterResolver and supporting beans as appropriate.
*
* @author Eric Bottard
*/
@Configuration
@ConditionalOnClass({ JCommander.class, JCommanderParameterResolver.class })
public class JCommanderParameterResolverAutoConfiguration {
@Bean
public JCommanderParameterResolver jCommanderParameterResolver() {
return new JCommanderParameterResolver();
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2021 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.impl.history.DefaultHistory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JLineAutoConfiguration {
@Configuration
@ConditionalOnMissingBean(org.jline.reader.History.class)
public static class JLineHistoryConfiguration {
@Bean
public org.jline.reader.History history(@Value("${spring.application.name:spring-shell}.log") String historyPath) {
return new DefaultHistory();
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2017-2021 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 java.io.IOException;
import org.jline.reader.Parser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.jline.ExtendedDefaultParser;
import org.springframework.shell.jline.PromptProvider;
/**
* Shell implementation using JLine to capture input and trigger completions.
*
* @author Eric Bottard
* @author Florent Biville
*/
@Configuration
public class JLineShellAutoConfiguration {
@Bean(destroyMethod = "close")
public Terminal terminal() {
try {
return TerminalBuilder.builder().build();
}
catch (IOException e) {
throw new BeanCreationException("Could not create Terminal: " + e.getMessage());
}
}
@Bean
@ConditionalOnMissingBean(PromptProvider.class)
public PromptProvider promptProvider() {
return () -> new AttributedString("shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
}
@Bean
public Parser parser() {
ExtendedDefaultParser parser = new ExtendedDefaultParser();
parser.setEofOnUnclosedQuote(true);
parser.setEofOnEscapedNewLine(true);
return parser;
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2021 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 java.io.IOException;
import java.nio.file.Paths;
import java.util.regex.Pattern;
import org.jline.reader.Completer;
import org.jline.reader.Highlighter;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.Parser;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.shell.CommandRegistry;
@Configuration
public class LineReaderAutoConfiguration {
@Autowired
private Terminal terminal;
@Autowired
private Completer completer;
@Autowired
private Parser parser;
@Autowired
private CommandRegistry commandRegistry;
@Autowired
private org.jline.reader.History jLineHistory;
@EventListener
public void onContextClosedEvent(ContextClosedEvent event) throws IOException {
jLineHistory.save();
}
@Value("${spring.application.name:spring-shell}.log")
private String historyPath;
@Bean
public LineReader lineReader() {
LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder()
.terminal(terminal)
.appName("Spring Shell")
.completer(completer)
.history(jLineHistory)
.highlighter(new Highlighter() {
@Override
public AttributedString highlight(LineReader reader, String buffer) {
int l = 0;
String best = null;
for (String command : commandRegistry.listCommands().keySet()) {
if (buffer.startsWith(command) && command.length() > l) {
l = command.length();
best = command;
}
}
if (best != null) {
return new AttributedStringBuilder(buffer.length()).append(best, AttributedStyle.BOLD).append(buffer.substring(l)).toAttributedString();
}
else {
return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
}
}
@Override
public void setErrorPattern(Pattern errorPattern) {
}
@Override
public void setErrorIndex(int errorIndex) {
}
})
.parser(parser);
LineReader lineReader = lineReaderBuilder.build();
lineReader.setVariable(LineReader.HISTORY_FILE, Paths.get(historyPath));
lineReader.unsetOpt(LineReader.Option.INSERT_TAB); // This allows completion on an empty buffer, rather than inserting a tab
jLineHistory.attach(lineReader);
return lineReader;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2017-2021 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 java.util.Collection;
import javax.validation.Validation;
import javax.validation.Validator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.shell.ResultHandler;
import org.springframework.shell.Shell;
import org.springframework.shell.result.IterableResultHandler;
import org.springframework.shell.result.ResultHandlerConfig;
/**
* Creates supporting beans for running the Shell
*/
@Configuration
@Import(ResultHandlerConfig.class)
public class SpringShellAutoConfiguration {
@Bean
@Qualifier("spring-shell")
public ConversionService shellConversionService(ApplicationContext applicationContext) {
Collection<Converter> converters = applicationContext.getBeansOfType(Converter.class).values();
Collection<GenericConverter> genericConverters = applicationContext.getBeansOfType(GenericConverter.class).values();
Collection<ConverterFactory> converterFactories = applicationContext.getBeansOfType(ConverterFactory.class).values();
DefaultConversionService defaultConversionService = new DefaultConversionService();
for (Converter converter : converters) {
defaultConversionService.addConverter(converter);
}
for (GenericConverter genericConverter : genericConverters) {
defaultConversionService.addConverter(genericConverter);
}
for (ConverterFactory converterFactory : converterFactories) {
defaultConversionService.addConverterFactory(converterFactory);
}
return defaultConversionService;
}
@Bean
@ConditionalOnMissingBean(Validator.class)
public Validator validator() {
return Validation.buildDefaultValidatorFactory().getValidator();
}
@Bean
public Shell shell(@Qualifier("main") ResultHandler resultHandler, @Qualifier("iterableResultHandler") IterableResultHandler iterableResultHandler) {
iterableResultHandler.setDelegate(resultHandler);
return new Shell(resultHandler);
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2017-2021 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 java.util.List;
import org.jline.reader.Parser;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.ParameterResolver;
import org.springframework.shell.Shell;
import org.springframework.shell.standard.commands.Clear;
import org.springframework.shell.standard.commands.Help;
import org.springframework.shell.standard.commands.History;
import org.springframework.shell.standard.commands.Quit;
import org.springframework.shell.standard.commands.Script;
import org.springframework.shell.standard.commands.Stacktrace;
/**
* Creates beans for standard commands.
*
* @author Eric Bottard
*/
@Configuration
@ConditionalOnClass({ Help.Command.class })
public class StandardCommandsAutoConfiguration {
@Bean
@ConditionalOnMissingBean(Help.Command.class)
@ConditionalOnProperty(prefix = "spring.shell.command.help", value = "enabled", havingValue = "true", matchIfMissing = true)
public Help help(List<ParameterResolver> parameterResolvers) {
return new Help(parameterResolvers);
}
@Bean
@ConditionalOnMissingBean(Clear.Command.class)
@ConditionalOnProperty(prefix = "spring.shell.command.clear", value = "enabled", havingValue = "true", matchIfMissing = true)
public Clear clear() {
return new Clear();
}
@Bean
@ConditionalOnMissingBean(Quit.Command.class)
@ConditionalOnProperty(prefix = "spring.shell.command.quit", value = "enabled", havingValue = "true", matchIfMissing = true)
public Quit quit() {
return new Quit();
}
@Bean
@ConditionalOnMissingBean(Stacktrace.Command.class)
@ConditionalOnProperty(prefix = "spring.shell.command.stacktrace", value = "enabled", havingValue = "true", matchIfMissing = true)
public Stacktrace stacktrace() {
return new Stacktrace();
}
@Bean
@ConditionalOnMissingBean(Script.Command.class)
@ConditionalOnProperty(prefix = "spring.shell.command.script", value = "enabled", havingValue = "true", matchIfMissing = true)
public Script script(ObjectProvider<Shell> shell, Parser parser) {
return new Script(shell, parser);
}
@Bean
@ConditionalOnMissingBean(History.Command.class)
@ConditionalOnProperty(prefix = "spring.shell.command.history", value = "enabled", havingValue = "true", matchIfMissing = true)
public History historyCommand(org.jline.reader.History jLineHistory) {
return new History(jLineHistory);
}
}

View File

@@ -0,0 +1,10 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.shell.boot.SpringShellAutoConfiguration,\
org.springframework.shell.boot.ApplicationRunnerAutoConfiguration,\
org.springframework.shell.boot.CommandRegistryAutoConfiguration,\
org.springframework.shell.boot.LineReaderAutoConfiguration,\
org.springframework.shell.boot.CompleterAutoConfiguration,\
org.springframework.shell.boot.JLineAutoConfiguration,\
org.springframework.shell.boot.JLineShellAutoConfiguration,\
org.springframework.shell.boot.JCommanderParameterResolverAutoConfiguration,\
org.springframework.shell.boot.StandardCommandsAutoConfiguration