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

@@ -1,58 +0,0 @@
/*
* 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;
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.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

@@ -1,34 +0,0 @@
/*
* 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;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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

@@ -1,80 +0,0 @@
/*
* 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;
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;
@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

@@ -1,37 +0,0 @@
/*
* 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;
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

@@ -1,109 +0,0 @@
/*
* 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;
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;
@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

@@ -1,76 +0,0 @@
/*
* Copyright 2017 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;
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.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

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-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.
@@ -20,6 +20,8 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -86,4 +88,16 @@ public class Utils {
.mapToObj(i -> createMethodParameter(executable, i));
}
/**
* Sanitize the buffer input given the customizations applied to the JLine
* parser (<em>e.g.</em> support for
* line continuations, <em>etc.</em>)
*/
public 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

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* 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.
@@ -35,7 +35,7 @@ import org.springframework.shell.CompletingParsedLine;
* @author Original JLine author
* @author Eric Bottard
*/
class ExtendedDefaultParser implements Parser {
public class ExtendedDefaultParser implements Parser {
private char[] quoteChars = { '\'', '"' };

View File

@@ -1,78 +0,0 @@
/*
* Copyright 2017 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.jline;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
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;
/**
* 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;
}
/**
* Sanitize the buffer input given the customizations applied to the JLine parser (<em>e.g.</em> support for
* line continuations, <em>etc.</em>)
*/
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

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* 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.
@@ -16,11 +16,13 @@
package org.springframework.shell.jline;
import org.jline.reader.ParsedLine;
import org.springframework.shell.Input;
import java.util.List;
import org.jline.reader.ParsedLine;
import org.springframework.shell.Input;
import org.springframework.shell.Utils;
/**
* An implementation of {@link Input} backed by the result of a {@link org.jline.reader.Parser#parse(String, int)}.
*
@@ -41,6 +43,6 @@ class ParsedLineInput implements Input {
@Override
public List<String> words() {
return JLineShellAutoConfiguration.sanitizeInput(parsedLine.words());
return Utils.sanitizeInput(parsedLine.words());
}
}

View File

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