Skip to content

Commit 918063b

Browse files
author
Nicola Di Falco
committed
feat: adding mandatory flag to string input
- Added to flow - Added to component - Add simple test case - Add samples
1 parent 0ab3785 commit 918063b

File tree

10 files changed

+160
-9
lines changed

10 files changed

+160
-9
lines changed

spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java

+61-6
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,28 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
4444
private final String defaultValue;
4545
private StringInputContext currentContext;
4646
private Character maskCharacter;
47+
private boolean mandatory;
4748

4849
public StringInput(Terminal terminal) {
49-
this(terminal, null, null, null);
50+
this(terminal, null, null, null, false);
5051
}
5152

5253
public StringInput(Terminal terminal, String name, String defaultValue) {
53-
this(terminal, name, defaultValue, null);
54+
this(terminal, name, defaultValue, null, false);
5455
}
5556

5657
public StringInput(Terminal terminal, String name, String defaultValue,
5758
Function<StringInputContext, List<AttributedString>> renderer) {
59+
this(terminal, name, defaultValue, renderer, false);
60+
}
61+
62+
public StringInput(Terminal terminal, String name, String defaultValue,
63+
Function<StringInputContext, List<AttributedString>> renderer, boolean mandatory) {
5864
super(terminal, name, null);
5965
setRenderer(renderer != null ? renderer : new DefaultRenderer());
6066
setTemplateLocation("classpath:org/springframework/shell/component/string-input-default.stg");
6167
this.defaultValue = defaultValue;
68+
this.mandatory = mandatory;
6269
}
6370

6471
/**
@@ -70,12 +77,21 @@ public void setMaskCharacter(Character maskCharacter) {
7077
this.maskCharacter = maskCharacter;
7178
}
7279

80+
/**
81+
* Sets a mandatory flag to check that the result is not empty
82+
*
83+
* @param mandatory if input is required
84+
*/
85+
public void setMandatory(boolean mandatory) {
86+
this.mandatory = mandatory;
87+
}
88+
7389
@Override
7490
public StringInputContext getThisContext(ComponentContext<?> context) {
7591
if (context != null && currentContext == context) {
7692
return currentContext;
7793
}
78-
currentContext = StringInputContext.of(defaultValue, maskCharacter);
94+
currentContext = StringInputContext.of(defaultValue, maskCharacter, mandatory);
7995
currentContext.setName(getName());
8096
context.stream().forEach(e -> {
8197
currentContext.put(e.getKey(), e.getValue());
@@ -116,6 +132,9 @@ protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, Strin
116132
}
117133
else if (context.getDefaultValue() != null) {
118134
context.setResultValue(context.getDefaultValue());
135+
} else if (mandatory) {
136+
context.setMessage("This field is mandatory", TextComponentContext.MessageLevel.ERROR);
137+
break;
119138
}
120139
return true;
121140
default:
@@ -175,13 +194,27 @@ public interface StringInputContext extends TextComponentContext<String, StringI
175194
*/
176195
Character getMaskCharacter();
177196

197+
/**
198+
* Sets flag for mandatory input.
199+
*
200+
* @param mandatory true if input is mandatory
201+
*/
202+
void setMandatory(boolean mandatory);
203+
204+
/**
205+
* Returns flag if input is required.
206+
*
207+
* @return true if input is required, false otherwise
208+
*/
209+
boolean isMandatory();
210+
178211
/**
179212
* Gets an empty {@link StringInputContext}.
180213
*
181214
* @return empty path input context
182215
*/
183216
public static StringInputContext empty() {
184-
return of(null, null);
217+
return of(null, null, false);
185218
}
186219

187220
/**
@@ -190,7 +223,16 @@ public static StringInputContext empty() {
190223
* @return path input context
191224
*/
192225
public static StringInputContext of(String defaultValue, Character maskCharacter) {
193-
return new DefaultStringInputContext(defaultValue, maskCharacter);
226+
return of(defaultValue, maskCharacter, false);
227+
}
228+
229+
/**
230+
* Gets an {@link StringInputContext}.
231+
*
232+
* @return path input context
233+
*/
234+
public static StringInputContext of(String defaultValue, Character maskCharacter, boolean mandatory) {
235+
return new DefaultStringInputContext(defaultValue, maskCharacter, mandatory);
194236
}
195237
}
196238

@@ -199,10 +241,12 @@ private static class DefaultStringInputContext extends BaseTextComponentContext<
199241

200242
private String defaultValue;
201243
private Character maskCharacter;
244+
private boolean mandatory;
202245

203-
public DefaultStringInputContext(String defaultValue, Character maskCharacter) {
246+
public DefaultStringInputContext(String defaultValue, Character maskCharacter, boolean mandatory) {
204247
this.defaultValue = defaultValue;
205248
this.maskCharacter = maskCharacter;
249+
this.mandatory = mandatory;
206250
}
207251

208252
@Override
@@ -220,6 +264,11 @@ public void setMaskCharacter(Character maskCharacter) {
220264
this.maskCharacter = maskCharacter;
221265
}
222266

267+
@Override
268+
public void setMandatory(boolean mandatory) {
269+
this.mandatory = mandatory;
270+
}
271+
223272
@Override
224273
public String getMaskedInput() {
225274
return maybeMask(getInput());
@@ -240,6 +289,11 @@ public Character getMaskCharacter() {
240289
return maskCharacter;
241290
}
242291

292+
@Override
293+
public boolean isMandatory() {
294+
return mandatory;
295+
}
296+
243297
@Override
244298
public Map<String, Object> toTemplateModel() {
245299
Map<String, Object> attributes = super.toTemplateModel();
@@ -248,6 +302,7 @@ public Map<String, Object> toTemplateModel() {
248302
attributes.put("maskedResultValue", getMaskedResultValue());
249303
attributes.put("maskCharacter", getMaskCharacter());
250304
attributes.put("hasMaskCharacter", hasMaskCharacter());
305+
attributes.put("mandatory", isMandatory());
251306
Map<String, Object> model = new HashMap<>();
252307
model.put("model", attributes);
253308
return model;

spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public abstract class BaseStringInput extends BaseInput<StringInputSpec> impleme
3838
private ResultMode resultMode;
3939
private String defaultValue;
4040
private Character maskCharacter;
41+
private boolean mandatory = false;
4142
private Function<StringInputContext, List<AttributedString>> renderer;
4243
private List<Consumer<StringInputContext>> preHandlers = new ArrayList<>();
4344
private List<Consumer<StringInputContext>> postHandlers = new ArrayList<>();
@@ -79,6 +80,12 @@ public StringInputSpec maskCharacter(Character maskCharacter) {
7980
return this;
8081
}
8182

83+
@Override
84+
public StringInputSpec mandatory() {
85+
this.mandatory = true;
86+
return this;
87+
}
88+
8289
@Override
8390
public StringInputSpec renderer(Function<StringInputContext, List<AttributedString>> renderer) {
8491
this.renderer = renderer;
@@ -146,6 +153,10 @@ public Character getMaskCharacter() {
146153
return maskCharacter;
147154
}
148155

156+
public boolean isMandatory() {
157+
return mandatory;
158+
}
159+
149160
public Function<StringInputContext, List<AttributedString>> getRenderer() {
150161
return renderer;
151162
}
@@ -169,4 +180,4 @@ public boolean isStoreResult() {
169180
public Function<StringInputContext, String> getNext() {
170181
return next;
171182
}
172-
}
183+
}

spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java

+1
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ private Stream<OrderedInputOperation> stringInputsStream() {
465465
if (input.getResultMode() == ResultMode.VERIFY && StringUtils.hasText(input.getResultValue())) {
466466
selector.addPreRunHandler(c -> {
467467
c.setDefaultValue(input.getResultValue());
468+
c.setMandatory(input.isMandatory());
468469
});
469470
}
470471
selector.addPostRunHandler(c -> {

spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ public interface StringInputSpec extends BaseInputSpec<StringInputSpec> {
7272
*/
7373
StringInputSpec maskCharacter(Character maskCharacter);
7474

75+
/**
76+
* Sets input to mandatory
77+
*
78+
* @return a builder
79+
*/
80+
StringInputSpec mandatory();
81+
7582
/**
7683
* Sets a renderer function.
7784
*
@@ -128,4 +135,4 @@ public interface StringInputSpec extends BaseInputSpec<StringInputSpec> {
128135
* @return the parent builder
129136
*/
130137
Builder and();
131-
}
138+
}

spring-shell-core/src/main/resources/org/springframework/shell/component/string-input-default.stg

+18
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1+
// message
2+
message(model) ::= <%
3+
<if(model.message && model.hasMessageLevelError)>
4+
<({<figures.error>}); format="style-level-error"> <model.message; format="style-level-error">
5+
<elseif(model.message && model.hasMessageLevelWarn)>
6+
<({<figures.warning>}); format="style-level-warn"> <model.message; format="style-level-warn">
7+
<elseif(model.message && model.hasMessageLevelInfo)>
8+
<({<figures.info>}); format="style-level-info"> <model.message; format="style-level-info">
9+
<endif>
10+
%>
11+
112
// info section after '? xxx'
213
info(model) ::= <%
314
<if(model.hasMaskCharacter)>
415
<if(model.maskedInput)>
516
<model.maskedInput>
617
<else>
18+
<if(model.mandatory)>
19+
<("[Mandatory]"); format="style-value">
20+
<endif>
721
<if(model.defaultValue)>
822
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
923
<endif>
@@ -12,6 +26,9 @@ info(model) ::= <%
1226
<if(model.input)>
1327
<model.input>
1428
<else>
29+
<if(model.mandatory)>
30+
<("[Mandatory]"); format="style-value">
31+
<endif>
1532
<if(model.defaultValue)>
1633
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
1734
<endif>
@@ -32,6 +49,7 @@ result(model) ::= <<
3249
// component is running
3350
running(model) ::= <<
3451
<question_name(model)> <info(model)>
52+
<message(model)>
3553
>>
3654

3755
// main

spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java

+33
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,39 @@ public void testResultUserInput() throws InterruptedException {
166166
assertThat(run1Context.getResultValue()).isEqualTo("test");
167167
}
168168

169+
@Test
170+
public void testResultMandatoryInput() throws InterruptedException {
171+
ComponentContext<?> empty = ComponentContext.empty();
172+
StringInput component1 = new StringInput(getTerminal());
173+
component1.setResourceLoader(new DefaultResourceLoader());
174+
component1.setTemplateExecutor(getTemplateExecutor());
175+
component1.setMandatory(true);
176+
177+
service.execute(() -> {
178+
StringInputContext run1Context = component1.run(empty);
179+
result1.set(run1Context);
180+
latch1.countDown();
181+
});
182+
183+
TestBuffer testBuffer = new TestBuffer().cr();
184+
write(testBuffer.getBytes());
185+
186+
latch1.await(2, TimeUnit.SECONDS);
187+
188+
StringInputContext run1Context = result1.get();
189+
assertThat(consoleOut()).contains("This field is mandatory");
190+
assertThat(run1Context).isNull();
191+
192+
testBuffer.append("test").cr();
193+
write(testBuffer.getBytes());
194+
195+
latch1.await(2, TimeUnit.SECONDS);
196+
run1Context = result1.get();
197+
198+
assertThat(run1Context).isNotNull();
199+
assertThat(run1Context.getResultValue()).isEqualTo("test");
200+
}
201+
169202
@Test
170203
public void testPassingViaContext() throws InterruptedException {
171204
ComponentContext<?> empty = ComponentContext.empty();

spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java

+9
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public void testSimpleFlow() throws InterruptedException {
5252
.withStringInput("field2")
5353
.name("Field2")
5454
.and()
55+
.withStringInput("field3")
56+
.name("Field3")
57+
.mandatory()
58+
.and()
5559
.withPathInput("path1")
5660
.name("Path1")
5761
.and()
@@ -80,6 +84,9 @@ public void testSimpleFlow() throws InterruptedException {
8084
// field2
8185
testBuffer = new TestBuffer().append("Field2Value").cr();
8286
write(testBuffer.getBytes());
87+
// field2
88+
testBuffer = new TestBuffer().cr().append("Field3Value").cr();
89+
write(testBuffer.getBytes());
8390
// path1
8491
testBuffer = new TestBuffer().append("fakedir").cr();
8592
write(testBuffer.getBytes());
@@ -95,10 +102,12 @@ public void testSimpleFlow() throws InterruptedException {
95102
assertThat(inputWizardResult).isNotNull();
96103
String field1 = inputWizardResult.getContext().get("field1");
97104
String field2 = inputWizardResult.getContext().get("field2");
105+
String field3 = inputWizardResult.getContext().get("field3");
98106
Path path1 = inputWizardResult.getContext().get("path1");
99107
String single1 = inputWizardResult.getContext().get("single1");
100108
List<String> multi1 = inputWizardResult.getContext().get("multi1");
101109
assertThat(field1).isEqualTo("defaultField1Value");
110+
assertThat(field3).isEqualTo("Field3Value");
102111
assertThat(field2).isEqualTo("Field2Value");
103112
assertThat(path1.toString()).contains("fakedir");
104113
assertThat(single1).isEqualTo("value1");

spring-shell-docs/src/main/asciidoc/using-shell-components-ui-stringinput.adoc

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
44

55
The string input component asks a user for simple text input, optionally masking values
6-
if the content contains something sensitive. The following listing shows an example:
6+
if the content contains something sensitive. The input can also be required (at least 1 char). +
7+
The following listing shows an example:
78

89
====
910
[source, java, indent=0]
@@ -38,6 +39,9 @@ The context object is `StringInputContext`. The following table lists its contex
3839
|`hasMaskCharacter`
3940
|`true` if a mask character is set. Otherwise, false.
4041

42+
|`mandatory`
43+
|`true` if the input is required. Otherwise, false.
44+
4145
|`model`
4246
|The parent context variables (see <<textcomponentcontext-template-variables>>).
4347
|===

spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java

+9
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ public String stringInput(boolean mask) {
5656
return "Got value " + context.getResultValue();
5757
}
5858

59+
@ShellMethod(key = "component string required", value = "String input", group = "Components")
60+
public String stringRequired() {
61+
StringInput component = new StringInput(getTerminal(), "Enter value", null, null, true);
62+
component.setResourceLoader(getResourceLoader());
63+
component.setTemplateExecutor(getTemplateExecutor());
64+
StringInputContext context = component.run(StringInputContext.empty());
65+
return "Got value " + context.getResultValue();
66+
}
67+
5968
@ShellMethod(key = "component path", value = "Path input", group = "Components")
6069
public String pathInput() {
6170
PathInput component = new PathInput(getTerminal(), "Enter value");

spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public void showcase1() {
6262
.withStringInput("field2")
6363
.name("Field2")
6464
.and()
65+
.withStringInput("field3")
66+
.name("Field3")
67+
.mandatory()
68+
.and()
6569
.withConfirmationInput("confirmation1")
6670
.name("Confirmation1")
6771
.and()

0 commit comments

Comments
 (0)