From a50b741af1ca9fac1a6cc795aca1282b95563b03 Mon Sep 17 00:00:00 2001 From: Eric Bottard Date: Tue, 19 Sep 2017 13:57:28 +0200 Subject: [PATCH] Document PromptProvider, ApplicationRunner and ConversionService. Make the default ConversionService register converters in the ctx --- .../shell/SpringShellAutoConfiguration.java | 24 +++- .../jline/DefaultShellApplicationRunner.java | 6 +- .../src/main/asciidoc/using-spring-shell.adoc | 126 +++++++++++++++++- .../samples/standard/ConversionExample.java | 49 +++++++ 4 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ConversionExample.java 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 f27d800c..2b3eb0ce 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 @@ -16,14 +16,20 @@ package org.springframework.shell; +import java.util.Collection; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; 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.ResultHandlerConfig; @@ -39,8 +45,22 @@ public class SpringShellAutoConfiguration { @Bean @ConditionalOnMissingBean(ConversionService.class) - public ConversionService conversionService() { - return new DefaultConversionService(); + public ConversionService conversionService(ApplicationContext applicationContext) { + Collection converters = applicationContext.getBeansOfType(Converter.class).values(); + Collection genericConverters = applicationContext.getBeansOfType(GenericConverter.class).values(); + Collection 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 diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/DefaultShellApplicationRunner.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/DefaultShellApplicationRunner.java index a48ba49c..404b7c37 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/DefaultShellApplicationRunner.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/DefaultShellApplicationRunner.java @@ -46,9 +46,10 @@ import org.springframework.shell.Shell; * * @author Eric Bottard */ +//tag::documentation[] @Order(DefaultShellApplicationRunner.PRECEDENCE) public class DefaultShellApplicationRunner implements ApplicationRunner { - +//end::documentation[] public static final int PRECEDENCE = 0; private final LineReader lineReader; @@ -66,6 +67,7 @@ public class DefaultShellApplicationRunner implements ApplicationRunner { this.shell = shell; } + //tag::documentation[] @Override public void run(ApplicationArguments args) throws Exception { List scriptsToRun = args.getNonOptionArgs().stream() @@ -84,7 +86,7 @@ public class DefaultShellApplicationRunner implements ApplicationRunner { } } } - + //end::documentation[] public static class JLineInputProvider implements InputProvider { private final LineReader lineReader; diff --git a/spring-shell-docs/src/main/asciidoc/using-spring-shell.adoc b/spring-shell-docs/src/main/asciidoc/using-spring-shell.adoc index 526ad255..ce54c128 100644 --- a/spring-shell-docs/src/main/asciidoc/using-spring-shell.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-spring-shell.adoc @@ -444,6 +444,7 @@ kbd:[Esc b] to move forward (_resp._ backward) one word at a time. TBD +[[validating-command-arguments]] === Validating Command Arguments Spring Shell integrates with the http://beanvalidation.org/[Bean Validation API] to support @@ -669,6 +670,7 @@ There are cases though when understanding what exactly happened is important (es To this purpose, Spring Shell remembers the last exception that occurred and the user can later use the `stacktrace` command to print all the gory details on the console. +[[script-command]] ==== Running a Batch of Commands The `script` command accepts a local file as an argument and will replay commands found there, one at a time. @@ -750,10 +752,132 @@ always welcome! ==== ResultHandlers ==== PromptProvider +After each command invocation, the shell waits for new input from the user, displaying +a _prompt_ in yellow: + +[source] +---- +shell:> +---- + +It is possible to customize this behavior by registering a bean of type `PromptProvider`. +Such a bean may use internal state to decide what to display to the user (it may for example +react to https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#context-functionality-events-annotation[application events]) +and can use JLine's `AttributedCharSequence` to display fancy ANSI text. + +Here is a fictional example: +[source, java] +---- +@Component +public class CustomPromptProvider implements PromptProvider { + + private ConnectionDetails connection; + + @Override + public AttributedString getPrompt() { + if (connection != null) { + return new AttributedString(connection.getHost() + ":>", + AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); + } + else { + return new AttributedString("server-unknown:>", + AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + } + } + + @EventListener + public void handle(ConnectionUpdatedEvent event) { + this.connection = event.getConnectionDetails(); + } +} +---- ==== Customizing Command Line Options Behavior +Spring Shell comes with a default Spring Boot `ApplicationRunner` +that bootstraps the Shell REPL. It sets up the JLine infrastructure and eventually +calls `Shell.run()`. -==== ConversionService +If the application is started with arguments that start with `@` though, it assumes those +are local file names and tries to run commands contained in those files (with the same +semantics as the xref:script-command[script command]) and then exits the process. + +If this behavior does not suit you, simply provide one (or more) bean of type `ApplicationRunner` +and it will replace the default. You'll want to take inspiration from the `DefaultShellApplicationRunner`: + +[source,java] +---- +include::../../../../spring-shell-core/src/main/java/org/springframework/shell/jline/DefaultShellApplicationRunner.java[tag=documentation] + +... +---- + +==== Customizing Arguments Conversion +Conversion from text input to actual method arguments uses the standard Spring +https://docs.spring.io/spring/docs/4.3.11.RELEASE/spring-framework-reference/htmlsingle/#core-convert[conversion] mechanism. +Spring Shell installs a new `DefaultConversionService` (with built-in converters enabled) +and registers to it any bean of type `Converter`, `GenericConverter` or +`ConverterFactory` that it finds in the application context. + +This means that it's really easy to customize conversion to your custom objects of type `Foo`: +just install a `Converter` bean in the context. + +[source, java] +---- +@ShellComponent +class ConversionCommands { + + @ShellMethod("Shows conversion using Spring converter") + public String conversionExample(DomainObject object) { + return object.getClass(); + } + +} + +class DomainObject { + private final String value; + + DomainObject(String value) { + this.value = value; + } + + public String toString() { + return value; + } +} + +@Component +class CustomDomainConverter implements Converter { + + @Override + public DomainObject convert(String source) { + return new DomainObject(source); + } +} + +---- + +[TIP] +.Mind your String representation +==== +As in the example above, it's probably a good idea if you can to have +your `toString()` implementations return the converse of what was used +to create the object instance. This is because when a value fails +validation, Spring Shell prints +[source] +---- +The following constraints were not met: + --arg : (You passed '') +---- +See xref:validating-command-arguments[] for more information. +==== + +[NOTE] +==== +If you want to customize the `ConversionService` further, you can either + +* Have the default one injected in your code and act upon it in some way +* Override it altogether with your own (custom converters will need to be registered by hand) +==== //==== Overriding the JLine Parser diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ConversionExample.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ConversionExample.java new file mode 100644 index 00000000..30a1bf5c --- /dev/null +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ConversionExample.java @@ -0,0 +1,49 @@ +/* + * 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 + * + * http://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.samples.standard; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import org.springframework.stereotype.Component; + +@ShellComponent +class ConversionCommands { + + @ShellMethod("Shows conversion using Spring converter") + public Object conversionExample(DomainObject object) { + return object; + } + +} + +class DomainObject { + private final String value; + + DomainObject(String value) { + this.value = value; + } +} + +@Component +class CustomDomainConverter implements Converter { + + @Override + public DomainObject convert(String source) { + return new DomainObject(source); + } +}