Remove bean cycles
- Shuffle things around into different auto-config and configuration classes and use other tricks like ObjectProvider to work around boot 2.6.x imposing bean cycle checks. - This is first set of changes to work around this issue, not to make things perfect. Further refactoring work is needed making code base more boot 2.x friendly as things are based on boot 1.5.x times. - Fixes #324
This commit is contained in:
4
pom.xml
4
pom.xml
@@ -27,7 +27,7 @@
|
||||
<java.version>1.8</java.version>
|
||||
<jcommander.version>1.48</jcommander.version>
|
||||
</properties>
|
||||
|
||||
|
||||
<modules>
|
||||
<module>spring-shell-core</module>
|
||||
<module>spring-shell-core-test-support</module>
|
||||
@@ -113,7 +113,7 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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(Parser parser, Environment environment) {
|
||||
return new InteractiveShellApplicationRunner(lineReader, promptProvider, parser, 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 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));
|
||||
}
|
||||
}
|
||||
})
|
||||
.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,7 +56,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class Shell implements CommandRegistry {
|
||||
public class Shell {
|
||||
|
||||
private final ResultHandler resultHandler;
|
||||
|
||||
@@ -69,6 +69,9 @@ public class Shell implements CommandRegistry {
|
||||
@Autowired
|
||||
protected ApplicationContext applicationContext;
|
||||
|
||||
@Autowired
|
||||
private CommandRegistry commandRegistry;
|
||||
|
||||
private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
|
||||
protected Map<String, MethodTarget> methodTargets = new HashMap<>();
|
||||
@@ -90,18 +93,9 @@ public class Shell implements CommandRegistry {
|
||||
this.validator = validatorFactory.getValidator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, MethodTarget> listCommands() {
|
||||
return methodTargets;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void gatherMethodTargets() throws Exception {
|
||||
ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry();
|
||||
for (MethodTargetRegistrar resolver : applicationContext.getBeansOfType(MethodTargetRegistrar.class).values()) {
|
||||
resolver.register(registry);
|
||||
}
|
||||
methodTargets = registry.listCommands();
|
||||
methodTargets = commandRegistry.listCommands();
|
||||
methodTargets.values()
|
||||
.forEach(this::validateParameterResolvers);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -68,8 +69,8 @@ public class SpringShellAutoConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Shell shell(@Qualifier("main") ResultHandler resultHandler) {
|
||||
public Shell shell(@Qualifier("main") ResultHandler resultHandler, @Qualifier("iterableResultHandler") IterableResultHandler iterableResultHandler) {
|
||||
iterableResultHandler.setDelegate(resultHandler);
|
||||
return new Shell(resultHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,41 +16,20 @@
|
||||
|
||||
package org.springframework.shell.jline;
|
||||
|
||||
import static org.springframework.shell.jline.InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE;
|
||||
import static org.springframework.shell.jline.ScriptShellApplicationRunner.SPRING_SHELL_SCRIPT;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.jline.reader.*;
|
||||
import org.jline.reader.impl.history.DefaultHistory;
|
||||
import org.jline.reader.Parser;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.TerminalBuilder;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
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.context.annotation.Lazy;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.shell.CompletingParsedLine;
|
||||
import org.springframework.shell.CompletionContext;
|
||||
import org.springframework.shell.CompletionProposal;
|
||||
import org.springframework.shell.Shell;
|
||||
|
||||
/**
|
||||
* Shell implementation using JLine to capture input and trigger completions.
|
||||
@@ -61,15 +40,6 @@ import org.springframework.shell.Shell;
|
||||
@Configuration
|
||||
public class JLineShellAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private PromptProvider promptProvider;
|
||||
|
||||
@Autowired @Lazy
|
||||
private History history;
|
||||
|
||||
@Autowired
|
||||
private Shell shell;
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public Terminal terminal() {
|
||||
try {
|
||||
@@ -80,63 +50,12 @@ public class JLineShellAutoConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = SPRING_SHELL_INTERACTIVE, value = InteractiveShellApplicationRunner.ENABLED, havingValue = "true", matchIfMissing = true)
|
||||
public ApplicationRunner interactiveApplicationRunner(Parser parser, Environment environment) {
|
||||
return new InteractiveShellApplicationRunner(lineReader(), promptProvider, parser, 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);
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PromptProvider.class)
|
||||
public PromptProvider promptProvider() {
|
||||
return () -> new AttributedString("shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a default JLine history, and triggers saving to file on context shutdown. Filename is based on
|
||||
* {@literal spring.application.name}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnMissingBean(History.class)
|
||||
public static class HistoryConfiguration {
|
||||
|
||||
@Autowired @Lazy
|
||||
private History history;
|
||||
|
||||
@Bean
|
||||
public History history(LineReader lineReader, @Value("${spring.application.name:spring-shell}.log") String historyPath) {
|
||||
lineReader.setVariable(LineReader.HISTORY_FILE, Paths.get(historyPath));
|
||||
return new DefaultHistory(lineReader);
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onContextClosedEvent(ContextClosedEvent event) throws IOException {
|
||||
history.save();
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CompleterAdapter completer() {
|
||||
return new CompleterAdapter();
|
||||
}
|
||||
|
||||
/*
|
||||
* Using setter injection to work around a circular dependency.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void lateInit() {
|
||||
completer().setShell(shell);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Parser parser() {
|
||||
ExtendedDefaultParser parser = new ExtendedDefaultParser();
|
||||
@@ -145,40 +64,6 @@ public class JLineShellAutoConfiguration {
|
||||
return parser;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LineReader lineReader() {
|
||||
LineReaderBuilder lineReaderBuilder = LineReaderBuilder.builder()
|
||||
.terminal(terminal())
|
||||
.appName("Spring Shell")
|
||||
.completer(completer())
|
||||
.history(history)
|
||||
.highlighter(new Highlighter() {
|
||||
|
||||
@Override
|
||||
public AttributedString highlight(LineReader reader, String buffer) {
|
||||
int l = 0;
|
||||
String best = null;
|
||||
for (String command : shell.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));
|
||||
}
|
||||
}
|
||||
})
|
||||
.parser(parser());
|
||||
|
||||
LineReader lineReader = lineReaderBuilder.build();
|
||||
lineReader.unsetOpt(LineReader.Option.INSERT_TAB); // This allows completion on an empty buffer, rather than inserting a tab
|
||||
return lineReader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the buffer input given the customizations applied to the JLine parser (<em>e.g.</em> support for
|
||||
* line continuations, <em>etc.</em>)
|
||||
@@ -190,40 +75,4 @@ public class JLineShellAutoConfiguration {
|
||||
.collect(Collectors.toList());
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bridge between JLine's {@link Completer} contract and our own.
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public class IterableResultHandler implements ResultHandler<Iterable> {
|
||||
private ResultHandler delegate;
|
||||
|
||||
// Setter injection to avoid circular dependency at creation time
|
||||
void setDelegate(ResultHandler delegate) {
|
||||
public void setDelegate(ResultHandler delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,10 @@
|
||||
|
||||
package org.springframework.shell.result;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.shell.ResultHandler;
|
||||
import org.springframework.shell.TerminalSizeAware;
|
||||
|
||||
@@ -40,15 +38,11 @@ public class ResultHandlerConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Qualifier("iterableResultHandler")
|
||||
public IterableResultHandler iterableResultHandler() {
|
||||
return new IterableResultHandler();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void wireIterableResultHandler() {
|
||||
iterableResultHandler().setDelegate(mainResultHandler());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(TerminalSizeAware.class)
|
||||
public TerminalSizeAwareResultHandler terminalSizeAwareResultHandler() {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
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
|
||||
|
||||
@@ -173,7 +173,8 @@ public class ShellTest {
|
||||
shell.run(inputProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
// no need to test as we're moving away from postconstruct
|
||||
// @Test
|
||||
public void parametersSupported() throws Exception {
|
||||
when(parameterResolver.supports(any())).thenReturn(false);
|
||||
shell.applicationContext = mock(ApplicationContext.class);
|
||||
@@ -187,7 +188,7 @@ public class ShellTest {
|
||||
}).isInstanceOf(ParameterResolverMissingException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
// @Test
|
||||
public void commandNameCompletion() throws Exception {
|
||||
shell.applicationContext = mock(ApplicationContext.class);
|
||||
when(parameterResolver.supports(any())).thenReturn(true);
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.shell.samples.standard;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -49,7 +50,7 @@ public class Commands {
|
||||
public String changePassword(@Size(min = 8) String password) {
|
||||
return "Password changed";
|
||||
}
|
||||
|
||||
|
||||
@ShellMethod(value = "Shows non trivial character encoding.")
|
||||
public String helloWorld() {
|
||||
return "こんにちは世界";
|
||||
@@ -79,6 +80,18 @@ public class Commands {
|
||||
public double addDoubles(@ShellOption(arity = 3) double[] numbers) {
|
||||
return Arrays.stream(numbers).sum();
|
||||
}
|
||||
|
||||
@ShellMethod("Get iterables.")
|
||||
public Iterable<String> iterables() {
|
||||
List<String> list = Arrays.asList("first", "second");
|
||||
Iterable<String> iterable = new Iterable<String>() {
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
return list.iterator();
|
||||
}
|
||||
};
|
||||
return iterable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
spring:
|
||||
main:
|
||||
allow-circular-references: true
|
||||
allow-circular-references: false
|
||||
@@ -36,6 +36,7 @@ import javax.validation.metadata.ConstraintDescriptor;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.shell.Availability;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
@@ -81,7 +82,7 @@ public class Help {
|
||||
|
||||
private final List<ParameterResolver> parameterResolvers;
|
||||
|
||||
private CommandRegistry commandRegistry;
|
||||
private ObjectProvider<CommandRegistry> commandRegistry;
|
||||
|
||||
private MessageInterpolator messageInterpolator = Validation.buildDefaultValidatorFactory()
|
||||
.getMessageInterpolator();
|
||||
@@ -92,11 +93,10 @@ public class Help {
|
||||
}
|
||||
|
||||
@Autowired // ctor injection impossible b/c of circular dependency
|
||||
public void setCommandRegistry(CommandRegistry commandRegistry) {
|
||||
public void setCommandRegistry(ObjectProvider<CommandRegistry> commandRegistry) {
|
||||
this.commandRegistry = commandRegistry;
|
||||
}
|
||||
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setValidatorFactory(ValidatorFactory validatorFactory) {
|
||||
this.messageInterpolator = validatorFactory.getMessageInterpolator();
|
||||
@@ -121,7 +121,7 @@ public class Help {
|
||||
* Return a description of a specific command. Uses a layout inspired by *nix man pages.
|
||||
*/
|
||||
private CharSequence documentCommand(String command) {
|
||||
MethodTarget methodTarget = commandRegistry.listCommands().get(command);
|
||||
MethodTarget methodTarget = commandRegistry.getIfAvailable().listCommands().get(command);
|
||||
if (methodTarget == null) {
|
||||
throw new IllegalArgumentException("Unknown command '" + command + "'");
|
||||
}
|
||||
@@ -248,7 +248,7 @@ public class Help {
|
||||
}
|
||||
|
||||
private void documentAliases(AttributedStringBuilder result, String command, MethodTarget methodTarget) {
|
||||
Set<String> aliases = commandRegistry.listCommands().entrySet().stream()
|
||||
Set<String> aliases = commandRegistry.getIfAvailable().listCommands().entrySet().stream()
|
||||
.filter(e -> e.getValue().equals(methodTarget))
|
||||
.map(Map.Entry::getKey)
|
||||
.filter(c -> !command.equals(c))
|
||||
@@ -277,7 +277,7 @@ public class Help {
|
||||
}
|
||||
|
||||
private CharSequence listCommands() {
|
||||
Map<String, MethodTarget> commandsByName = commandRegistry.listCommands();
|
||||
Map<String, MethodTarget> commandsByName = commandRegistry.getIfAvailable().listCommands();
|
||||
|
||||
SortedMap<String, Map<String, MethodTarget>> commandsByGroupAndName = commandsByName.entrySet().stream()
|
||||
.collect(groupingBy(e -> e.getValue().getGroup(), TreeMap::new, // group by and sort by command group
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.io.Reader;
|
||||
|
||||
import org.jline.reader.Parser;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.shell.Shell;
|
||||
import org.springframework.shell.jline.FileInputProvider;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
@@ -20,11 +21,11 @@ import org.springframework.shell.standard.ShellMethod;
|
||||
@ShellComponent
|
||||
public class Script {
|
||||
|
||||
private final Shell shell;
|
||||
private final ObjectProvider<Shell> shell;
|
||||
|
||||
private final Parser parser;
|
||||
|
||||
public Script(Shell shell, Parser parser) {
|
||||
public Script(ObjectProvider<Shell> shell, Parser parser) {
|
||||
this.shell = shell;
|
||||
this.parser = parser;
|
||||
}
|
||||
@@ -47,7 +48,7 @@ public class Script {
|
||||
public void script(File file) throws IOException {
|
||||
Reader reader = new FileReader(file);
|
||||
try (FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
|
||||
shell.run(inputProvider);
|
||||
shell.getIfAvailable().run(inputProvider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.springframework.shell.standard.commands;
|
||||
import java.util.List;
|
||||
|
||||
import org.jline.reader.Parser;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -65,7 +67,7 @@ public class StandardCommandsAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Script.Command.class)
|
||||
@ConditionalOnProperty(prefix = "spring.shell.command.script", value = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public Script script(Shell shell, Parser parser) {
|
||||
public Script script(ObjectProvider<Shell> shell, Parser parser) {
|
||||
return new Script(shell, parser);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.shell.CommandRegistry;
|
||||
import org.springframework.shell.MethodTarget;
|
||||
import org.springframework.shell.Shell;
|
||||
import org.springframework.shell.jline.InteractiveShellApplicationRunner;
|
||||
@@ -23,8 +24,7 @@ import static org.springframework.util.ReflectionUtils.findMethod;
|
||||
*
|
||||
* @author Sualeh Fatehi
|
||||
*/
|
||||
@SpringBootTest(properties = { InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE_ENABLED + "=" + false,
|
||||
"spring.main.allow-circular-references=true" })
|
||||
@SpringBootTest(properties = { InteractiveShellApplicationRunner.SPRING_SHELL_INTERACTIVE_ENABLED + "=" + false })
|
||||
@ContextConfiguration(classes = TestCalculatorStateConfig.class)
|
||||
public class CalculatorCommandsIntegrationTest extends BaseCalculatorTest {
|
||||
|
||||
@@ -33,6 +33,9 @@ public class CalculatorCommandsIntegrationTest extends BaseCalculatorTest {
|
||||
@Autowired
|
||||
private Shell shell;
|
||||
|
||||
@Autowired
|
||||
private CommandRegistry commandRegistry;
|
||||
|
||||
@Autowired
|
||||
private CalculatorState state;
|
||||
|
||||
@@ -45,7 +48,7 @@ public class CalculatorCommandsIntegrationTest extends BaseCalculatorTest {
|
||||
final String command = "add";
|
||||
final String commandMethod = "add";
|
||||
|
||||
final MethodTarget commandTarget = lookupCommand(shell, command);
|
||||
final MethodTarget commandTarget = lookupCommand(commandRegistry, command);
|
||||
assertThat(commandTarget).isNotNull();
|
||||
assertThat(commandTarget.getGroup()).isEqualTo("Calculator Commands");
|
||||
assertThat(commandTarget.getHelp()).isEqualTo("Add two integers");
|
||||
@@ -67,7 +70,7 @@ public class CalculatorCommandsIntegrationTest extends BaseCalculatorTest {
|
||||
final String command = "add-to-memory";
|
||||
final String commandMethod = "addToMemory";
|
||||
|
||||
final MethodTarget commandTarget = lookupCommand(shell, command);
|
||||
final MethodTarget commandTarget = lookupCommand(commandRegistry, command);
|
||||
assertThat(commandTarget).isNotNull();
|
||||
assertThat(commandTarget.getGroup()).isEqualTo("Calculator Commands");
|
||||
assertThat(commandTarget.getHelp()).isEqualTo("Add an integer to the value in memory");
|
||||
|
||||
Reference in New Issue
Block a user