diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/StringInput.java b/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/StringInput.java index f244912b3..c9126d899 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/StringInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/StringInput.java @@ -49,20 +49,28 @@ public class StringInput extends AbstractTextComponent> renderer) { + this(terminal, name, defaultValue, renderer, false); + } + + public StringInput(Terminal terminal, @Nullable String name, @Nullable String defaultValue, + @Nullable Function> renderer, boolean required) { super(terminal, name, null); setRenderer(renderer != null ? renderer : new DefaultRenderer()); setTemplateLocation("classpath:org/springframework/shell/component/string-input-default.stg"); this.defaultValue = defaultValue; + this.required = required; } /** @@ -73,12 +81,21 @@ public void setMaskCharacter(@Nullable Character maskCharacter) { this.maskCharacter = maskCharacter; } + /** + * Sets a required flag to check that the result is not empty + * + * @param required if input is required + */ + public void setRequired(boolean required) { + this.required = required; + } + @Override public StringInputContext getThisContext(@Nullable ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } - currentContext = StringInputContext.of(defaultValue, maskCharacter); + currentContext = StringInputContext.of(defaultValue, maskCharacter, required); currentContext.setName(getName()); if (context != null) { context.stream().forEach(e -> { @@ -124,6 +141,9 @@ protected boolean read(BindingReader bindingReader, KeyMap keyMap, Strin } else if (context.getDefaultValue() != null) { context.setResultValue(context.getDefaultValue()); + } else if (required) { + context.setMessage("This field is mandatory", TextComponentContext.MessageLevel.ERROR); + break; } return true; default: @@ -176,12 +196,26 @@ public interface StringInputContext extends TextComponentContext toTemplateModel() { Map attributes = super.toTemplateModel(); @@ -249,6 +305,7 @@ public boolean hasMaskCharacter() { attributes.put("maskedResultValue", getMaskedResultValue()); attributes.put("maskCharacter", getMaskCharacter()); attributes.put("hasMaskCharacter", hasMaskCharacter()); + attributes.put("required", isRequired()); Map model = new HashMap<>(); model.put("model", attributes); return model; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/BaseStringInput.java b/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/BaseStringInput.java index aa4e0964f..bcfb88d88 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/BaseStringInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/BaseStringInput.java @@ -45,6 +45,8 @@ public abstract class BaseStringInput extends BaseInput impleme private @Nullable Character maskCharacter; + private boolean required = false; + private @Nullable Function> renderer; private List> preHandlers = new ArrayList<>(); @@ -91,6 +93,12 @@ public StringInputSpec maskCharacter(Character maskCharacter) { return this; } + @Override + public StringInputSpec required() { + this.required = true; + return this; + } + @Override public StringInputSpec renderer(Function> renderer) { this.renderer = renderer; @@ -158,6 +166,10 @@ public StringInputSpec getThis() { return maskCharacter; } + public boolean isRequired() { + return required; + } + public @Nullable Function> getRenderer() { return renderer; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/ComponentFlow.java b/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/ComponentFlow.java index f3bdefd31..5fc15ffc3 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/ComponentFlow.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/ComponentFlow.java @@ -468,7 +468,7 @@ else if (n.isPresent()) { private Stream stringInputsStream() { return stringInputs.stream().map(input -> { - StringInput selector = new StringInput(terminal, input.getName(), input.getDefaultValue()); + StringInput selector = new StringInput(terminal, input.getName(), input.getDefaultValue(), null, input.isRequired()); Function, ComponentContext> operation = (context) -> { if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult() && StringUtils.hasText(input.getResultValue())) { @@ -492,6 +492,7 @@ private Stream stringInputsStream() { if (input.getResultMode() == ResultMode.VERIFY && StringUtils.hasText(input.getResultValue())) { selector.addPreRunHandler(c -> { c.setDefaultValue(input.getResultValue()); + c.setRequired(input.isRequired()); }); } selector.addPostRunHandler(c -> { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/StringInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/StringInputSpec.java index 124664ecd..4323cd74e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/StringInputSpec.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/core/tui/component/flow/StringInputSpec.java @@ -67,6 +67,13 @@ public interface StringInputSpec extends BaseInputSpec { */ StringInputSpec maskCharacter(Character maskCharacter); + /** + * Sets input to required + * + * @return a builder + */ + StringInputSpec required(); + /** * Sets a renderer function. * @param renderer the renderer diff --git a/spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/StringInputTests.java b/spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/StringInputTests.java index db478c4db..64c1ca92d 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/StringInputTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/StringInputTests.java @@ -162,6 +162,39 @@ void testResultUserInput() { }); } + @Test + void testResultMandatoryInput() { + ComponentContext empty = ComponentContext.empty(); + StringInput component1 = new StringInput(getTerminal()); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + component1.setRequired(true); + + service.execute(() -> { + StringInputContext run1Context = component1.run(empty); + result1.set(run1Context); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + await().atMost(Duration.ofSeconds(2)).untilAsserted(() -> { + StringInputContext run1Context = result1.get(); + assertThat(consoleOut()).contains("This field is mandatory"); + assertThat(run1Context).isNull(); + }); + + testBuffer.append("test").cr(); + write(testBuffer.getBytes()); + + await().atMost(Duration.ofSeconds(2)).untilAsserted(() -> { + StringInputContext run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isEqualTo("test"); + }); + } + @Test void testResultUserInputUnicode() { ComponentContext empty = ComponentContext.empty(); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/flow/ComponentFlowTests.java b/spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/flow/ComponentFlowTests.java index 87553d8a9..37a0183a8 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/flow/ComponentFlowTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/core/tui/component/flow/ComponentFlowTests.java @@ -54,6 +54,10 @@ void testSimpleFlow() { .withStringInput("field2") .name("Field2") .and() + .withStringInput("field3") + .name("Field3") + .required() + .and() .withPathInput("path1") .name("Path1") .and() @@ -78,6 +82,9 @@ void testSimpleFlow() { // field2 testBuffer = new TestBuffer().append("Field2Value").cr(); write(testBuffer.getBytes()); + // field3 + testBuffer = new TestBuffer().cr().append("Field3Value").cr(); + write(testBuffer.getBytes()); // path1 testBuffer = new TestBuffer().append("fakedir").cr(); write(testBuffer.getBytes()); @@ -93,17 +100,18 @@ void testSimpleFlow() { assertThat(inputWizardResult).isNotNull(); String field1 = inputWizardResult.getContext().get("field1"); String field2 = inputWizardResult.getContext().get("field2"); + String field3 = inputWizardResult.getContext().get("field3"); Path path1 = inputWizardResult.getContext().get("path1"); String single1 = inputWizardResult.getContext().get("single1"); List multi1 = inputWizardResult.getContext().get("multi1"); assertThat(field1).isEqualTo("defaultField1Value"); + assertThat(field3).isEqualTo("Field3Value"); assertThat(field2).isEqualTo("Field2Value"); assertThat(path1.toString()).contains("fakedir"); assertThat(single1).isEqualTo("value1"); assertThat(multi1).containsExactlyInAnyOrder("value2"); assertThat(consoleOut()).contains("Field1 defaultField1Value"); }); - } @Test diff --git a/spring-shell-core/src/test/resources/org/springframework/shell/component/string-input-default.stg b/spring-shell-core/src/test/resources/org/springframework/shell/component/string-input-default.stg index 265163fa4..58872a089 100644 --- a/spring-shell-core/src/test/resources/org/springframework/shell/component/string-input-default.stg +++ b/spring-shell-core/src/test/resources/org/springframework/shell/component/string-input-default.stg @@ -1,3 +1,14 @@ +// message +message(model) ::= <% + + <({}); format="style-level-error"> + + <({}); format="style-level-warn"> + + <({}); format="style-level-info"> + +%> + // info section after '? xxx' info(model) ::= <% @@ -6,6 +17,8 @@ info(model) ::= <% <("[Default "); format="style-value"><("]"); format="style-value"> + + <("[Required]"); format="style-value"> @@ -14,6 +27,8 @@ info(model) ::= <% <("[Default "); format="style-value"><("]"); format="style-value"> + + <("[Required]"); format="style-value"> @@ -32,6 +47,7 @@ result(model) ::= << // component is running running(model) ::= << + >> // main diff --git a/spring-shell-docs/modules/ROOT/pages/components/ui/stringinput.adoc b/spring-shell-docs/modules/ROOT/pages/components/ui/stringinput.adoc index 96c54f29f..dad421317 100644 --- a/spring-shell-docs/modules/ROOT/pages/components/ui/stringinput.adoc +++ b/spring-shell-docs/modules/ROOT/pages/components/ui/stringinput.adoc @@ -4,7 +4,8 @@ ifndef::snippets[:snippets: ../../../../../src/test/java/org/springframework/shell/docs] The string input component asks a user for simple text input, optionally masking values -if the content contains something sensitive. The following listing shows an example: +if the content contains something sensitive. The input can also be required (at least 1 char). + +The following listing shows an example: [source, java, indent=0] ---- @@ -40,6 +41,9 @@ The context object is `StringInputContext`. The following table lists its contex |`hasMaskCharacter` |`true` if a mask character is set. Otherwise, false. +|`required` +|`true` if the input is required. Otherwise, false. + |`model` |The parent context variables (see xref:/components/ui/render.adoc#textcomponentcontext-template-variables[TextComponentContext Template Variables]). |=== diff --git a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java index 8ad545039..a1679bd6d 100644 --- a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java +++ b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java @@ -61,6 +61,10 @@ public void showcase1() { .withStringInput("field2") .name("Field2") .and() + .withStringInput("field3") + .name("Field3") + .mandatory() + .and() .withConfirmationInput("confirmation1") .name("Confirmation1") .and()