diff --git a/pom.xml b/pom.xml index 41ed2efa..f43d89ee 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 1.8 1.48 - + spring-shell-core spring-shell-core-test-support @@ -113,7 +113,7 @@ org.springframework.boot spring-boot-maven-plugin - + diff --git a/spring-shell-core/src/main/java/org/springframework/shell/ApplicationRunnerAutoConfiguration.java b/spring-shell-core/src/main/java/org/springframework/shell/ApplicationRunnerAutoConfiguration.java new file mode 100644 index 00000000..2c3eb38f --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/ApplicationRunnerAutoConfiguration.java @@ -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); + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/CommandRegistryAutoConfiguration.java b/spring-shell-core/src/main/java/org/springframework/shell/CommandRegistryAutoConfiguration.java new file mode 100644 index 00000000..eabdc23a --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/CommandRegistryAutoConfiguration.java @@ -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 methodTargerRegistrars) { + ConfigurableCommandRegistry registry = new ConfigurableCommandRegistry(); + methodTargerRegistrars.orderedStream().forEach(resolver -> { + resolver.register(registry); + }); + return registry; + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/CompleterAutoConfiguration.java b/spring-shell-core/src/main/java/org/springframework/shell/CompleterAutoConfiguration.java new file mode 100644 index 00000000..50c81b5f --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/CompleterAutoConfiguration.java @@ -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 candidates) { + CompletingParsedLine cpl = (line instanceof CompletingParsedLine) ? ((CompletingParsedLine) line) : t -> t; + + CompletionContext context = new CompletionContext(sanitizeInput(line.words()), line.wordIndex(), line.wordCursor()); + + List 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 sanitizeInput(List 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; + } + } + +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/JLineAutoConfiguration.java b/spring-shell-core/src/main/java/org/springframework/shell/JLineAutoConfiguration.java new file mode 100644 index 00000000..bb74f0ba --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/JLineAutoConfiguration.java @@ -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(); + } + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/LineReaderAutoConfiguration.java b/spring-shell-core/src/main/java/org/springframework/shell/LineReaderAutoConfiguration.java new file mode 100644 index 00000000..933d71f3 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/LineReaderAutoConfiguration.java @@ -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; + } + +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java index f26ca9cc..50505c11 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java @@ -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 methodTargets = new HashMap<>(); @@ -90,18 +93,9 @@ public class Shell implements CommandRegistry { this.validator = validatorFactory.getValidator(); } - @Override - public Map 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); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/SpringShellAutoConfiguration.java b/spring-shell-core/src/main/java/org/springframework/shell/SpringShellAutoConfiguration.java index c55e9e0d..70726f07 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/SpringShellAutoConfiguration.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/SpringShellAutoConfiguration.java @@ -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); } - } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/JLineShellAutoConfiguration.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/JLineShellAutoConfiguration.java index ca29cc90..6d123821 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/JLineShellAutoConfiguration.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/JLineShellAutoConfiguration.java @@ -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 (e.g. support for * line continuations, etc.) @@ -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 candidates) { - CompletingParsedLine cpl = (line instanceof CompletingParsedLine) ? ((CompletingParsedLine) line) : t -> t; - - CompletionContext context = new CompletionContext(sanitizeInput(line.words()), line.wordIndex(), line.wordCursor()); - - List 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; - } - } - - } - diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/IterableResultHandler.java b/spring-shell-core/src/main/java/org/springframework/shell/result/IterableResultHandler.java index ec397bb9..fbf8db3d 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/IterableResultHandler.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/IterableResultHandler.java @@ -29,7 +29,7 @@ public class IterableResultHandler implements ResultHandler { private ResultHandler delegate; // Setter injection to avoid circular dependency at creation time - void setDelegate(ResultHandler delegate) { + public void setDelegate(ResultHandler delegate) { this.delegate = delegate; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java index 07bc401a..978e09b3 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java @@ -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() { diff --git a/spring-shell-core/src/main/resources/META-INF/spring.factories b/spring-shell-core/src/main/resources/META-INF/spring.factories index 066c65bc..15d3e03e 100644 --- a/spring-shell-core/src/main/resources/META-INF/spring.factories +++ b/spring-shell-core/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/spring-shell-core/src/test/java/org/springframework/shell/ShellTest.java b/spring-shell-core/src/test/java/org/springframework/shell/ShellTest.java index a2a1f638..92ef6749 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/ShellTest.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/ShellTest.java @@ -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); diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/Commands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/Commands.java index e4b557d9..3d1fa070 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/Commands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/Commands.java @@ -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 iterables() { + List list = Arrays.asList("first", "second"); + Iterable iterable = new Iterable() { + @Override + public Iterator iterator() { + return list.iterator(); + } + }; + return iterable; + } } /** diff --git a/spring-shell-samples/src/main/resources/application.yml b/spring-shell-samples/src/main/resources/application.yml index 2d5afe0b..60b6d1a6 100644 --- a/spring-shell-samples/src/main/resources/application.yml +++ b/spring-shell-samples/src/main/resources/application.yml @@ -1,3 +1,3 @@ spring: main: - allow-circular-references: true \ No newline at end of file + allow-circular-references: false \ No newline at end of file diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Help.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Help.java index 628dda86..773f3999 100644 --- a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Help.java +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Help.java @@ -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 parameterResolvers; - private CommandRegistry commandRegistry; + private ObjectProvider 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) { 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 aliases = commandRegistry.listCommands().entrySet().stream() + Set 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 commandsByName = commandRegistry.listCommands(); + Map commandsByName = commandRegistry.getIfAvailable().listCommands(); SortedMap> commandsByGroupAndName = commandsByName.entrySet().stream() .collect(groupingBy(e -> e.getValue().getGroup(), TreeMap::new, // group by and sort by command group diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Script.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Script.java index ba14e341..0b31e60d 100644 --- a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Script.java +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Script.java @@ -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; private final Parser parser; - public Script(Shell shell, Parser parser) { + public Script(ObjectProvider 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); } } diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/StandardCommandsAutoConfiguration.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/StandardCommandsAutoConfiguration.java index 539e5b86..1d2e262b 100644 --- a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/StandardCommandsAutoConfiguration.java +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/StandardCommandsAutoConfiguration.java @@ -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, Parser parser) { return new Script(shell, parser); } diff --git a/spring-shell-test-samples/src/test/java/com/example/test/integration/CalculatorCommandsIntegrationTest.java b/spring-shell-test-samples/src/test/java/com/example/test/integration/CalculatorCommandsIntegrationTest.java index 02a61dec..a84e2cfa 100644 --- a/spring-shell-test-samples/src/test/java/com/example/test/integration/CalculatorCommandsIntegrationTest.java +++ b/spring-shell-test-samples/src/test/java/com/example/test/integration/CalculatorCommandsIntegrationTest.java @@ -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");