From 93cd27f125ae346406c49b39ce284234057f4632 Mon Sep 17 00:00:00 2001 From: Nicola Di Falco Date: Fri, 4 Aug 2023 14:51:24 +0200 Subject: [PATCH] feat: adding mandatory flag to string input - Added to flow - Added to component - Add simple test case - Add samples --- .../shell/component/StringInput.java | 67 +++++++++++++++++-- .../shell/component/flow/BaseStringInput.java | 13 +++- .../shell/component/flow/ComponentFlow.java | 3 +- .../shell/component/flow/StringInputSpec.java | 9 ++- .../shell/component/string-input-default.stg | 18 +++++ .../shell/component/StringInputTests.java | 33 +++++++++ .../component/flow/ComponentFlowTests.java | 9 +++ ...using-shell-components-ui-stringinput.adoc | 6 +- .../samples/standard/ComponentCommands.java | 9 +++ .../standard/ComponentFlowCommands.java | 4 ++ 10 files changed, 161 insertions(+), 10 deletions(-) diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java index e94e9d148..44607f055 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java @@ -44,21 +44,28 @@ public class StringInput extends AbstractTextComponent> renderer) { + this(terminal, name, defaultValue, renderer, false); + } + + public StringInput(Terminal terminal, String name, String defaultValue, + Function> renderer, boolean mandatory) { super(terminal, name, null); setRenderer(renderer != null ? renderer : new DefaultRenderer()); setTemplateLocation("classpath:org/springframework/shell/component/string-input-default.stg"); this.defaultValue = defaultValue; + this.mandatory = mandatory; } /** @@ -70,12 +77,21 @@ public void setMaskCharacter(Character maskCharacter) { this.maskCharacter = maskCharacter; } + /** + * Sets a mandatory flag to check that the result is not empty + * + * @param mandatory if input is required + */ + public void setMandatory(boolean mandatory) { + this.mandatory = mandatory; + } + @Override public StringInputContext getThisContext(ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } - currentContext = StringInputContext.of(defaultValue, maskCharacter); + currentContext = StringInputContext.of(defaultValue, maskCharacter, mandatory); currentContext.setName(getName()); context.stream().forEach(e -> { currentContext.put(e.getKey(), e.getValue()); @@ -116,6 +132,9 @@ protected boolean read(BindingReader bindingReader, KeyMap keyMap, Strin } else if (context.getDefaultValue() != null) { context.setResultValue(context.getDefaultValue()); + } else if (mandatory) { + context.setMessage("This field is mandatory", TextComponentContext.MessageLevel.ERROR); + break; } return true; default: @@ -175,13 +194,27 @@ public interface StringInputContext extends TextComponentContext toTemplateModel() { Map attributes = super.toTemplateModel(); @@ -248,6 +302,7 @@ public Map toTemplateModel() { attributes.put("maskedResultValue", getMaskedResultValue()); attributes.put("maskCharacter", getMaskCharacter()); attributes.put("hasMaskCharacter", hasMaskCharacter()); + attributes.put("mandatory", isMandatory()); Map model = new HashMap<>(); model.put("model", attributes); return model; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java index 14e165466..17aad93bb 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java @@ -38,6 +38,7 @@ public abstract class BaseStringInput extends BaseInput impleme private ResultMode resultMode; private String defaultValue; private Character maskCharacter; + private boolean mandatory = false; private Function> renderer; private List> preHandlers = new ArrayList<>(); private List> postHandlers = new ArrayList<>(); @@ -79,6 +80,12 @@ public StringInputSpec maskCharacter(Character maskCharacter) { return this; } + @Override + public StringInputSpec mandatory() { + this.mandatory = true; + return this; + } + @Override public StringInputSpec renderer(Function> renderer) { this.renderer = renderer; @@ -146,6 +153,10 @@ public Character getMaskCharacter() { return maskCharacter; } + public boolean isMandatory() { + return mandatory; + } + public Function> getRenderer() { return renderer; } @@ -169,4 +180,4 @@ public boolean isStoreResult() { public Function getNext() { return next; } -} \ No newline at end of file +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java index d1ee2c83f..6ce38865c 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java @@ -445,7 +445,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.isMandatory()); Function, ComponentContext> operation = (context) -> { if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult() && StringUtils.hasText(input.getResultValue())) { @@ -465,6 +465,7 @@ private Stream stringInputsStream() { if (input.getResultMode() == ResultMode.VERIFY && StringUtils.hasText(input.getResultValue())) { selector.addPreRunHandler(c -> { c.setDefaultValue(input.getResultValue()); + c.setMandatory(input.isMandatory()); }); } selector.addPostRunHandler(c -> { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java index c21a0be86..ab639bfc1 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java @@ -72,6 +72,13 @@ public interface StringInputSpec extends BaseInputSpec { */ StringInputSpec maskCharacter(Character maskCharacter); + /** + * Sets input to mandatory + * + * @return a builder + */ + StringInputSpec mandatory(); + /** * Sets a renderer function. * @@ -128,4 +135,4 @@ public interface StringInputSpec extends BaseInputSpec { * @return the parent builder */ Builder and(); -} \ No newline at end of file +} diff --git a/spring-shell-core/src/main/resources/org/springframework/shell/component/string-input-default.stg b/spring-shell-core/src/main/resources/org/springframework/shell/component/string-input-default.stg index 265163fa4..a013b71bb 100644 --- a/spring-shell-core/src/main/resources/org/springframework/shell/component/string-input-default.stg +++ b/spring-shell-core/src/main/resources/org/springframework/shell/component/string-input-default.stg @@ -1,9 +1,23 @@ +// message +message(model) ::= <% + + <({}); format="style-level-error"> + + <({}); format="style-level-warn"> + + <({}); format="style-level-info"> + +%> + // info section after '? xxx' info(model) ::= <% + + <("[Mandatory]"); format="style-value"> + <("[Default "); format="style-value"><("]"); format="style-value"> @@ -12,6 +26,9 @@ info(model) ::= <% + + <("[Mandatory]"); format="style-value"> + <("[Default "); format="style-value"><("]"); format="style-value"> @@ -32,6 +49,7 @@ result(model) ::= << // component is running running(model) ::= << + >> // main diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java index 1ff6f29cf..341bae819 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java @@ -166,6 +166,39 @@ public void testResultUserInput() throws InterruptedException { assertThat(run1Context.getResultValue()).isEqualTo("test"); } + @Test + public void testResultMandatoryInput() throws InterruptedException { + ComponentContext empty = ComponentContext.empty(); + StringInput component1 = new StringInput(getTerminal()); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + component1.setMandatory(true); + + service.execute(() -> { + StringInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + + StringInputContext run1Context = result1.get(); + assertThat(consoleOut()).contains("This field is mandatory"); + assertThat(run1Context).isNull(); + + testBuffer.append("test").cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isEqualTo("test"); + } + @Test public void testPassingViaContext() throws InterruptedException { ComponentContext empty = ComponentContext.empty(); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java index e58e2e70d..9e232536b 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java @@ -52,6 +52,10 @@ public void testSimpleFlow() throws InterruptedException { .withStringInput("field2") .name("Field2") .and() + .withStringInput("field3") + .name("Field3") + .mandatory() + .and() .withPathInput("path1") .name("Path1") .and() @@ -80,6 +84,9 @@ public void testSimpleFlow() throws InterruptedException { // 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()); @@ -95,10 +102,12 @@ public void testSimpleFlow() throws InterruptedException { 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"); diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-components-ui-stringinput.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-components-ui-stringinput.adoc index b88574a64..a74f50c05 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-components-ui-stringinput.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-components-ui-stringinput.adoc @@ -3,7 +3,8 @@ ifndef::snippets[:snippets: ../../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] @@ -38,6 +39,9 @@ The context object is `StringInputContext`. The following table lists its contex |`hasMaskCharacter` |`true` if a mask character is set. Otherwise, false. +|`mandatory` +|`true` if the input is required. Otherwise, false. + |`model` |The parent context variables (see <>). |=== diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java index a85e994d6..ad404f55e 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java @@ -56,6 +56,15 @@ public String stringInput(boolean mask) { return "Got value " + context.getResultValue(); } + @ShellMethod(key = "component string required", value = "String input", group = "Components") + public String stringRequired() { + StringInput component = new StringInput(getTerminal(), "Enter value", null, null, true); + component.setResourceLoader(getResourceLoader()); + component.setTemplateExecutor(getTemplateExecutor()); + StringInputContext context = component.run(StringInputContext.empty()); + return "Got value " + context.getResultValue(); + } + @ShellMethod(key = "component path", value = "Path input", group = "Components") public String pathInput() { PathInput component = new PathInput(getTerminal(), "Enter value"); diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java index 696c1e001..8d5cb3897 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java @@ -62,6 +62,10 @@ public void showcase1() { .withStringInput("field2") .name("Field2") .and() + .withStringInput("field3") + .name("Field3") + .mandatory() + .and() .withConfirmationInput("confirmation1") .name("Confirmation1") .and()