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..fa370f5ec 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 @@ -26,7 +26,6 @@ import org.jline.utils.AttributedString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.shell.component.StringInput.StringInputContext; import org.springframework.shell.component.context.ComponentContext; import org.springframework.shell.component.support.AbstractTextComponent; @@ -44,21 +43,28 @@ public class StringInput extends AbstractTextComponent> renderer) { + this(terminal, name, defaultValue, renderer, false); + } + + public StringInput(Terminal terminal, String name, String defaultValue, + 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; } /** @@ -70,12 +76,21 @@ public void setMaskCharacter(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(ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } - currentContext = StringInputContext.of(defaultValue, maskCharacter); + currentContext = StringInputContext.of(defaultValue, maskCharacter, required); currentContext.setName(getName()); context.stream().forEach(e -> { currentContext.put(e.getKey(), e.getValue()); @@ -116,6 +131,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: @@ -175,13 +193,27 @@ public interface StringInputContext extends TextComponentContext toTemplateModel() { Map attributes = super.toTemplateModel(); @@ -248,6 +301,7 @@ public Map toTemplateModel() { 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/component/flow/BaseStringInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java index 14e165466..78fc0dde0 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 @@ -21,7 +21,6 @@ import java.util.function.Function; import org.jline.utils.AttributedString; - import org.springframework.shell.component.StringInput.StringInputContext; import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder; import org.springframework.shell.component.flow.ComponentFlow.Builder; @@ -38,6 +37,7 @@ public abstract class BaseStringInput extends BaseInput impleme private ResultMode resultMode; private String defaultValue; private Character maskCharacter; + private boolean required = false; private Function> renderer; private List> preHandlers = new ArrayList<>(); private List> postHandlers = new ArrayList<>(); @@ -79,6 +79,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; @@ -146,6 +152,10 @@ public Character getMaskCharacter() { return maskCharacter; } + public boolean isRequired() { + return required; + } + public Function> getRenderer() { return renderer; } @@ -169,4 +179,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..f5d9dede5 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 @@ -31,11 +31,11 @@ import org.jline.terminal.Terminal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.core.io.ResourceLoader; import org.springframework.shell.component.ConfirmationInput; +import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext; import org.springframework.shell.component.MultiItemSelector; import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext; import org.springframework.shell.component.PathInput; @@ -43,7 +43,6 @@ import org.springframework.shell.component.SingleItemSelector; import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext; import org.springframework.shell.component.StringInput; -import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext; import org.springframework.shell.component.StringInput.StringInputContext; import org.springframework.shell.component.context.ComponentContext; import org.springframework.shell.component.support.SelectorItem; @@ -445,7 +444,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())) { @@ -465,6 +464,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/component/flow/StringInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java index c21a0be86..3628895bc 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 @@ -20,7 +20,6 @@ import java.util.function.Function; import org.jline.utils.AttributedString; - import org.springframework.shell.component.StringInput.StringInputContext; import org.springframework.shell.component.context.ComponentContext; import org.springframework.shell.component.flow.ComponentFlow.Builder; @@ -72,6 +71,13 @@ public interface StringInputSpec extends BaseInputSpec { */ StringInputSpec maskCharacter(Character maskCharacter); + /** + * Sets input to required + * + * @return a builder + */ + StringInputSpec required(); + /** * Sets a renderer function. * @@ -128,4 +134,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..58872a089 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,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-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..d09964509 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 @@ -15,6 +15,8 @@ */ package org.springframework.shell.component; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; @@ -28,13 +30,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.core.io.DefaultResourceLoader; import org.springframework.shell.component.StringInput.StringInputContext; import org.springframework.shell.component.context.ComponentContext; -import static org.assertj.core.api.Assertions.assertThat; - public class StringInputTests extends AbstractShellTests { private ExecutorService service; @@ -166,6 +165,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.setRequired(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..bff644de8 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 @@ -15,6 +15,8 @@ */ package org.springframework.shell.component.flow; +import static org.assertj.core.api.Assertions.assertThat; + import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; @@ -27,11 +29,8 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; - import org.springframework.shell.component.flow.ComponentFlow.ComponentFlowResult; -import static org.assertj.core.api.Assertions.assertThat; - public class ComponentFlowTests extends AbstractShellTests { @Test @@ -52,6 +51,10 @@ public void testSimpleFlow() throws InterruptedException { .withStringInput("field2") .name("Field2") .and() + .withStringInput("field3") + .name("Field3") + .required() + .and() .withPathInput("path1") .name("Path1") .and() @@ -80,6 +83,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 +101,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..ba05ef07f 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. +|`required` +|`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..64ba5b09c 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 @@ -39,16 +39,18 @@ import org.springframework.shell.standard.AbstractShellComponent; import org.springframework.shell.standard.ShellComponent; import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellOption; import org.springframework.util.StringUtils; @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"); + public String stringInput(boolean mask, boolean required, @ShellOption(defaultValue = ShellOption.NULL) String defaultValue) { + StringInput component = new StringInput(getTerminal(), "Enter value", defaultValue); component.setResourceLoader(getResourceLoader()); component.setTemplateExecutor(getTemplateExecutor()); + component.setRequired(required); if (mask) { component.setMaskCharacter('*'); } 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..0a49157d0 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 @@ -24,7 +24,6 @@ import java.util.stream.IntStream; import org.jline.terminal.impl.DumbTerminal; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException; @@ -62,6 +61,10 @@ public void showcase1() { .withStringInput("field2") .name("Field2") .and() + .withStringInput("field3") + .name("Field3") + .required() + .and() .withConfirmationInput("confirmation1") .name("Confirmation1") .and()