Rework command subsystem

- Focus of these changes are to introduce a new command system based on
  real registrations (new way) instead of continuously (old way) resolve
  methods and its parameters via reflection.
- There's a lot of changes as this resolution via reflection had its
  hooks almost everywhere and thus most changes are just refactorings.
- Order to understand real changes I'd start to look classes under
  `org.springframework.shell.command` package as it defines new registration,
  catalog and parser classes. Also samples contain new classes to demonstrate
  new functionality.
- Fixes #380
This commit is contained in:
Janne Valkealahti
2022-05-06 08:32:53 +01:00
parent 81e5bf8c81
commit 8a23518b84
91 changed files with 7026 additions and 2291 deletions

View File

@@ -0,0 +1,46 @@
=== Command Catalog
`CommandCatalog` is an interface defining how command registrations exists in
a shell application. It is possible to dynamically register and de-register
commands which gives flexibility for a user cases where possible commands will
come and go depending on a states shell is at.
====
[source, java, indent=0]
----
include::{snippets}/CommandCatalogSnippets.java[tag=snippet1]
----
====
==== Command Resolver
`CommandResolver` is an interface you can implement and define as a bean to dynamically
resolve mappings from a command names to its `CommandRegistration` instances. Its use
case looks something like:
====
[source, java, indent=0]
----
include::{snippets}/CommandCatalogSnippets.java[tag=snippet2]
----
====
[IMPORTANT]
====
Current limitation of a `CommandResolver` is that it is used every time commands are resolved.
Thus it's adviced not to use it if command resolve call takes a long time as it would
make shell feel sluggish.
====
==== Command Catalog Customizer
`CommandCatalogCustomizer` is an interface which can be used to customize a `CommandCatalog`.
Its main use case is to modify catalog and within `spring-shell` _auto-configuration_ this
interface is used to register existing `CommandRegistration` beans into a catalog.
Its use case looks something like:
====
[source, java, indent=0]
----
include::{snippets}/CommandCatalogSnippets.java[tag=snippet3]
----
====
Create `CommandCatalogCustomizer` as a bean and `spring-shell` will handle rest.

View File

@@ -0,0 +1,20 @@
=== Command Context
`CommandContext` is an interface which gives access to a currently executing
context. It can be used to get access to options:
====
[source, java, indent=0]
----
include::{snippets}/CommandContextSnippets.java[tag=snippet1]
----
====
If you need to print something into a shell you can get `Terminal`
and use its writer to print something:
====
[source, java, indent=0]
----
include::{snippets}/CommandContextSnippets.java[tag=snippet2]
----
====

View File

@@ -0,0 +1,3 @@
=== Command Execution
When _command parsing_ has done its job togethere with resolving _command registration_, execution
will do the hard work and execute a real user level code.

View File

@@ -0,0 +1,3 @@
=== Command Parser
Before a command can be executed we need to parse commands and options provided by a user. Parsing
sits between _command registration_ and _command execution_.

View File

@@ -0,0 +1,106 @@
[#appendix-tech-intro-registration]
=== Command Registration
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Defining a command registation is a first step to introduce a structure of a commands and its options
and parameters. This is loosely decoupled what happens later like parsing command-line and executing
actual target code. Essentially it is a definition of an command API shown to a user.
==== Commands
Command in a `spring-shell` structure is defined as an array of commands. This will give
you something like:
====
[source, bash]
----
command1 sub1
command2 sub1 subsub1
command2 sub2 subsub1
command2 sub2 subsub2
----
====
[NOTE]
====
We don't currently support mapping commands to explicit parent if sub-commands are defined.
For example there can't be `command1 sub1` and `command1 sub1 subsub1` registered.
====
==== Interaction Mode
Spring Shell has been designed to work on two modes one being interactive which essentially
is a `REPL` where you have an active shell instance throughout commands and secondly
non-interactive mode where commands are executed one by one from a command line.
Differentation between these modes are mostly around limitations what can be done
in each mode as for example it would not be feasible to show what was a previous stacktrace
of a command if shell is not alive anymore and generally things around information
if shell is alive or not.
Also being on an active `REPL` session may provide more info about what user has been
doing within an active session.
==== Options
Options can be defined as long and short where prefixing is `--` and `-` respectively.
====
[source, java, indent=0]
----
include::{snippets}/CommandRegistrationSnippets.java[tag=snippet1]
----
====
====
[source, java, indent=0]
----
include::{snippets}/CommandRegistrationSnippets.java[tag=snippet2]
----
====
==== Target
Target defines what is an execution target of a command. It can be a _method_ in a `POJO`,
`Consumer` or `Function`.
===== Method
Using a `Method` is a way to define target as a method in an existing pojo.
====
[source, java, indent=0]
----
include::{snippets}/CommandTargetSnippets.java[tag=snippet11]
----
====
Having existing class shown above you can then register its method.
====
[source, java, indent=0]
----
include::{snippets}/CommandTargetSnippets.java[tag=snippet12]
----
====
===== Function
Using a `Function` as a target gives a lot of flexibility to handle what
happens in a command execution as you can handle many things manually using
a `CommandContext` given to a `Function`. Return type from a `Function` is
then what gets printed into a shell as a result.
====
[source, java, indent=0]
----
include::{snippets}/CommandTargetSnippets.java[tag=snippet2]
----
====
===== Consumer
Using a `Consumer` is basically same as `Function` with difference being
that there is not return type. If you need to print something into a shell
you can get a reference to a `Terminal` from a context and print something
through it.
====
[source, java, indent=0]
----
include::{snippets}/CommandTargetSnippets.java[tag=snippet3]
----
====

View File

@@ -0,0 +1,15 @@
[appendix]
[#appendix-tech-intro]
== Techical Introduction
This section contains information for a developers and others who would like to know more about how _spring-shell_
internally works and what are its design decisions.
include::appendices-techical-intro-registration.adoc[]
include::appendices-techical-intro-parser.adoc[]
include::appendices-techical-intro-execution.adoc[]
include::appendices-techical-intro-commandcontext.adoc[]
include::appendices-techical-intro-commandcatalog.adoc[]

View File

@@ -0,0 +1 @@
include::appendices-techical-intro.adoc[]

View File

@@ -20,3 +20,5 @@ include::introduction.adoc[]
include::getting-started.adoc[]
include::using-shell.adoc[]
include::appendices.adoc[]

View File

@@ -0,0 +1,14 @@
[[using-shell-basics]]
=== Basics
You are here to learn basics of a _spring shell_. Before going forward to define actual _commands_ and _options_
lets take this moment to go trough some fundamental concepts of a _spring shell_.
Essentially few things needs to happen before you have a working _spring shell_ app:
- Create a _spring boot_ application
- Define commands and its option
- Package an application
- Execute either interactively or non-interactively
You will get a full working _spring shell_ application without defining any user level commands
as some basic build-in commands are provided out of a box like `help` and `history`.

View File

@@ -1,13 +1,21 @@
=== Writing Your Own Commands
=== Define a Command
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
The way Spring Shell decides to turn a method into an actual shell command is entirely pluggable
(see <<extending-spring-shell>>). However, as of Spring Shell 2.x, the recommended way to write commands
is to use the new API described in this section (the standard API).
In this section we go through an actual command registration and leave command options
and execution later in a documentation. More detailed info can be found from
<<appendix-tech-intro-registration>>.
There are two different ways to define a command. Firstly through an annotation model and
secondly through programmatic model. Annotation model if where you define your methods
in a class and annotate class and methods with a spesific annotations. Programmatic model
is where things are done on a more low level ways by defining command registrations either
as beans or registering those with a command catalog dynamically.
==== Annotation Model
When you use the standard API, methods on beans are turned into executable commands, provided that:
* The bean class bears the `@ShellComponent` annotation. This is used to restrict the set of beans that
are considered.
* The bean class bears the `@ShellComponent` annotation. This is used to restrict the set of beans
that are considered.
* The method bears the `@ShellMethod` annotation.
[TIP]
@@ -18,17 +26,22 @@ you can used it in addition to the filtering mechanism to declare beans (for exa
You can customize the name of the created bean by using the `value` attribute of the annotation.
====
[[documenting-the-command]]
==== Documenting the Command
====
[source, java, indent=0]
----
include::{snippets}/AnnotationRegistrationSnippets.java[tag=snippet1]
----
====
The only required attribute of the `@ShellMethod` annotation is its `value` attribute, which should have
a short, one-sentence, description of what the command does. This lets your users
get consistent help about your commands without having to leave the shell (see <<help-command>>).
NOTE: The description of your command should be short -- no more than one or two sentences. For better
[NOTE]
====
The description of your command should be short -- no more than one or two sentences. For better
consistency, it should starts with a capital letter and end with a period.
==== Customizing the Command Name(s)
====
By default, there is no need to specify the key for your command (that is, the word(s) that should be used
to invoke it in the shell). The name of the method is used as the command key, turning camelCase names into
@@ -37,19 +50,31 @@ dashed, gnu-style, names (that is, `sayHello()` becomes `say-hello`).
You can, however, explicitly set the command key, by using the `key` attribute of the annotation:
====
[source, java]
[source, java, indent=0]
----
@ShellMethod(value = "Add numbers.", key = "sum")
public int add(int a, int b) {
return a + b;
}
include::{snippets}/AnnotationRegistrationSnippets.java[tag=snippet2]
----
====
NOTE: The `key` attribute accepts multiple values.
[NOTE]
====
The `key` attribute accepts multiple values.
If you set multiple keys for a single method, the command is registered with those different aliases.
====
TIP: The command key can contain pretty much any character, including spaces. When coming up with names though,
[TIP]
====
The command key can contain pretty much any character, including spaces. When coming up with names though,
keep in mind that consistency is often appreciated by users (that is, you should avoid mixing dashed-names with
spaced names and other inconsistencies).
====
==== Programmatic Model
`CommandRegistration` can be defined as a `@Bean` and it's automatically registered.
====
[source, java, indent=0]
----
include::{snippets}/CommandRegistrationBeanSnippets.java[tag=snippet1]
----
====

View File

@@ -1,5 +1,6 @@
[[uicomponents]]
=== UI Components
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Starting from _2.1.x_ there is a new component model which provides
easier way to create higher level user interaction for usual use cases
@@ -32,49 +33,18 @@ via code then gives you flexibility to do whatever you need.
Programmatic way to render is simple as to create a `Function`:
====
[source, java]
[source, java, indent=0]
----
class StringInputCustomRenderer implements Function<StringInputContext, List<AttributedString>> {
@Override
public List<AttributedString> apply(StringInputContext context) {
AttributedStringBuilder builder = new AttributedStringBuilder();
builder.append(context.getName());
builder.append(" ");
if (context.getResultValue() != null) {
builder.append(context.getResultValue());
}
else {
String input = context.getInput();
if (StringUtils.hasText(input)) {
builder.append(input);
}
else {
builder.append("[Default " + context.getDefaultValue() + "]");
}
}
return Arrays.asList(builder.toAttributedString());
}
}
include::{snippets}/UiComponentSnippets.java[tag=snippet1]
----
====
And then hook it with a component:
====
[source, java]
[source, java, indent=0]
----
@ShellMethod(key = "component stringcustom", value = "String input", group = "Components")
public String stringInputCustom(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue",
new StringInputCustomRenderer());
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharater('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
include::{snippets}/UiComponentSnippets.java[tag=snippet2]
----
====
@@ -147,26 +117,13 @@ Used to ask a simple text input from a user, optionally masking values
if content contains something sensitive.
====
[source, java]
[source, java, indent=0]
----
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component string", value = "String input", group = "Components")
public String stringInput(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharater('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
}
include::{snippets}/UiComponentSnippets.java[tag=snippet3]
----
====
image::images/component-text-input-1.svg[text input]
Context object is `StringInputContext`.
@@ -200,20 +157,9 @@ Context object is `StringInputContext`.
Used to ask a `Path` from a user and gives additional info about a path itself.
====
[source, java]
[source, java, indent=0]
----
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component path", value = "Path input", group = "Components")
public String pathInput() {
PathInput component = new PathInput(getTerminal(), "Enter value");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
PathInputContext context = component.run(PathInputContext.empty());
return "Got value " + context.getResultValue();
}
}
include::{snippets}/UiComponentSnippets.java[tag=snippet4]
----
====
@@ -236,20 +182,9 @@ Used to ask a simple confirmation from a user and essentially is
yes/no question.
====
[source, java]
[source, java, indent=0]
----
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
public String confirmationInput(boolean no) {
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
ConfirmationInputContext context = component.run(ConfirmationInputContext.empty());
return "Got value " + context.getResultValue();
}
}
include::{snippets}/UiComponentSnippets.java[tag=snippet5]
----
====
@@ -275,26 +210,9 @@ Used to ask an item from a list and is essentially similar to simple
dropbox implementation.
====
[source, java]
[source, java, indent=0]
----
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
public String singleSelector() {
List<SelectorItem<String>> items = new ArrayList<>();
items.add(SelectorItem.of("key1", "value1"));
items.add(SelectorItem.of("key2", "value2"));
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
SingleItemSelectorContext<String, SelectorItem<String>> context = component
.run(SingleItemSelectorContext.empty());
String result = context.getResultItem().flatMap(si -> Optional.ofNullable(si.getItem())).get();
return "Got value " + result;
}
}
include::{snippets}/UiComponentSnippets.java[tag=snippet6]
----
====
@@ -322,29 +240,9 @@ Context object is `SingleItemSelectorContext`.
Used to ask an items from a list.
====
[source, java]
[source, java, indent=0]
----
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
public String multiSelector() {
List<SelectorItem<String>> items = new ArrayList<>();
items.add(SelectorItem.of("key1", "value1"));
items.add(SelectorItem.of("key2", "value2", false));
items.add(SelectorItem.of("key3", "value3"));
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
MultiItemSelectorContext<String, SelectorItem<String>> context = component
.run(MultiItemSelectorContext.empty());
String result = context.getResultItems().stream()
.map(si -> si.getItem())
.collect(Collectors.joining(","));
return "Got value " + result;
}
}
include::{snippets}/UiComponentSnippets.java[tag=snippet7]
----
====

View File

@@ -10,7 +10,9 @@ more relevant on a java space. Moving to new major version also allows
us to clean up codebase and make some needed breaking changes.
====
include::using-shell-write-command.adoc[]
include::using-shell-basics.adoc[]
include::using-shell-define-command.adoc[]
include::using-shell-invoke-command.adoc[]

View File

@@ -0,0 +1,27 @@
package org.springframework.shell.docs;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
public class AnnotationRegistrationSnippets {
// tag::snippet1[]
@ShellComponent
static class MyCommands {
@ShellMethod
public void mycommand() {
}
}
// end::snippet1[]
static class Dump1 {
// tag::snippet2[]
@ShellMethod(value = "Add numbers.", key = "sum")
public int add(int a, int b) {
return a + b;
}
// end::snippet2[]
}
}

View File

@@ -0,0 +1,52 @@
package org.springframework.shell.docs;
import java.util.ArrayList;
import java.util.List;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.command.CommandCatalogCustomizer;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandResolver;
public class CommandCatalogSnippets {
CommandCatalog catalog = CommandCatalog.of();
void dump1() {
// tag::snippet1[]
CommandRegistration registration = CommandRegistration.builder().build();
catalog.register(registration);
// end::snippet1[]
}
// tag::snippet2[]
static class CustomCommandResolver implements CommandResolver {
List<CommandRegistration> registrations = new ArrayList<>();
CustomCommandResolver() {
CommandRegistration resolved = CommandRegistration.builder()
.command("resolve command")
.build();
registrations.add(resolved);
}
@Override
public List<CommandRegistration> resolve() {
return registrations;
}
}
// end::snippet2[]
// tag::snippet3[]
static class CustomCommandCatalogCustomizer implements CommandCatalogCustomizer {
@Override
public void customize(CommandCatalog commandCatalog) {
CommandRegistration registration = CommandRegistration.builder()
.command("resolve command")
.build();
commandCatalog.register(registration);
}
}
// end::snippet3[]
}

View File

@@ -0,0 +1,24 @@
package org.springframework.shell.docs;
import javax.validation.constraints.Null;
import org.springframework.shell.command.CommandContext;
@SuppressWarnings("unused")
public class CommandContextSnippets {
CommandContext ctx = CommandContext.of(null, null, null);
void dump1() {
// tag::snippet1[]
String arg = ctx.getOptionValue("arg");
// end::snippet1[]
}
void dump2() {
// tag::snippet2[]
ctx.getTerminal().writer().println("hi");
// end::snippet2[]
}
}

View File

@@ -0,0 +1,16 @@
package org.springframework.shell.docs;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
public class CommandRegistrationBeanSnippets {
// tag::snippet1[]
@Bean
CommandRegistration commandRegistration() {
return CommandRegistration.builder()
.command("mycommand")
.build();
}
// end::snippet1[]
}

View File

@@ -0,0 +1,26 @@
package org.springframework.shell.docs;
import org.springframework.shell.command.CommandRegistration;
public class CommandRegistrationSnippets {
void dump1() {
// tag::snippet1[]
CommandRegistration.builder()
.withOption()
.longNames("myopt")
.and()
.build();
// end::snippet1[]
}
void dump2() {
// tag::snippet2[]
CommandRegistration.builder()
.withOption()
.shortNames('s')
.and()
.build();
// end::snippet2[]
}
}

View File

@@ -0,0 +1,65 @@
package org.springframework.shell.docs;
import org.springframework.shell.command.CommandRegistration;
public class CommandTargetSnippets {
// tag::snippet11[]
public static class CommandPojo {
String command(String arg) {
return arg;
}
}
// end::snippet11[]
void dump1() {
// tag::snippet12[]
CommandPojo pojo = new CommandPojo();
CommandRegistration.builder()
.command("command")
.withTarget()
.method(pojo, "command")
.and()
.withOption()
.longNames("arg")
.and()
.build();
// end::snippet12[]
}
void dump2() {
// tag::snippet2[]
CommandRegistration.builder()
.command("command")
.withTarget()
.function(ctx -> {
String arg = ctx.getOptionValue("arg");
return String.format("hi, arg value is '%s'", arg);
})
.and()
.withOption()
.longNames("arg")
.and()
.build();
// end::snippet2[]
}
void dump3() {
// tag::snippet3[]
CommandRegistration.builder()
.command("command")
.withTarget()
.consumer(ctx -> {
String arg = ctx.getOptionValue("arg");
ctx.getTerminal().writer()
.println(String.format("hi, arg value is '%s'", arg));
})
.and()
.withOption()
.longNames("arg")
.and()
.build();
// end::snippet3[]
}
}

View File

@@ -0,0 +1,174 @@
package org.springframework.shell.docs;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.springframework.shell.component.ConfirmationInput;
import org.springframework.shell.component.MultiItemSelector;
import org.springframework.shell.component.PathInput;
import org.springframework.shell.component.SingleItemSelector;
import org.springframework.shell.component.StringInput;
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
import org.springframework.shell.component.PathInput.PathInputContext;
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
import org.springframework.shell.component.StringInput.StringInputContext;
import org.springframework.shell.component.support.SelectorItem;
import org.springframework.shell.standard.AbstractShellComponent;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.util.StringUtils;
public class UiComponentSnippets {
// tag::snippet1[]
class StringInputCustomRenderer implements Function<StringInputContext, List<AttributedString>> {
@Override
public List<AttributedString> apply(StringInputContext context) {
AttributedStringBuilder builder = new AttributedStringBuilder();
builder.append(context.getName());
builder.append(" ");
if (context.getResultValue() != null) {
builder.append(context.getResultValue());
}
else {
String input = context.getInput();
if (StringUtils.hasText(input)) {
builder.append(input);
}
else {
builder.append("[Default " + context.getDefaultValue() + "]");
}
}
return Arrays.asList(builder.toAttributedString());
}
}
// end::snippet1[]
class Dump1 extends AbstractShellComponent {
// tag::snippet2[]
@ShellMethod(key = "component stringcustom", value = "String input", group = "Components")
public String stringInputCustom(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue",
new StringInputCustomRenderer());
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharater('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
// end::snippet2[]
}
class Dump2 {
// tag::snippet3[]
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component string", value = "String input", group = "Components")
public String stringInput(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharater('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
}
// end::snippet3[]
}
class Dump3 {
// tag::snippet4[]
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component path", value = "Path input", group = "Components")
public String pathInput() {
PathInput component = new PathInput(getTerminal(), "Enter value");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
PathInputContext context = component.run(PathInputContext.empty());
return "Got value " + context.getResultValue();
}
}
// end::snippet4[]
}
class Dump4 {
// tag::snippet5[]
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
public String confirmationInput(boolean no) {
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
ConfirmationInputContext context = component.run(ConfirmationInputContext.empty());
return "Got value " + context.getResultValue();
}
}
// end::snippet5[]
}
class Dump5 {
// tag::snippet6[]
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
public String singleSelector() {
List<SelectorItem<String>> items = new ArrayList<>();
items.add(SelectorItem.of("key1", "value1"));
items.add(SelectorItem.of("key2", "value2"));
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
SingleItemSelectorContext<String, SelectorItem<String>> context = component
.run(SingleItemSelectorContext.empty());
String result = context.getResultItem().flatMap(si -> Optional.ofNullable(si.getItem())).get();
return "Got value " + result;
}
}
// end::snippet6[]
}
class Dump6 {
// tag::snippet7[]
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
public String multiSelector() {
List<SelectorItem<String>> items = new ArrayList<>();
items.add(SelectorItem.of("key1", "value1"));
items.add(SelectorItem.of("key2", "value2", false));
items.add(SelectorItem.of("key3", "value3"));
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
MultiItemSelectorContext<String, SelectorItem<String>> context = component
.run(MultiItemSelectorContext.empty());
String result = context.getResultItems().stream()
.map(si -> si.getItem())
.collect(Collectors.joining(","));
return "Got value " + result;
}
}
// end::snippet7[]
}
}