Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit efa19cd

Browse files
Nicola Di FalcoNicola Di Falco
authored andcommittedApr 25, 2025
feat: adding number input type
1 parent 2f00d73 commit efa19cd

File tree

10 files changed

+1179
-10
lines changed

10 files changed

+1179
-10
lines changed
 
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.component;
17+
18+
import java.util.HashMap;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Optional;
22+
import java.util.function.Function;
23+
24+
import org.jline.keymap.BindingReader;
25+
import org.jline.keymap.KeyMap;
26+
import org.jline.terminal.Terminal;
27+
import org.jline.utils.AttributedString;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
import org.springframework.shell.component.NumberInput.NumberInputContext;
31+
import org.springframework.shell.component.context.ComponentContext;
32+
import org.springframework.shell.component.support.AbstractTextComponent;
33+
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext.MessageLevel;
34+
import org.springframework.util.NumberUtils;
35+
import org.springframework.util.StringUtils;
36+
37+
/**
38+
* Component for a number input.
39+
*
40+
* @author Nicola Di Falco
41+
*/
42+
public class NumberInput extends AbstractTextComponent<Number, NumberInputContext> {
43+
44+
private static final Logger log = LoggerFactory.getLogger(NumberInput.class);
45+
private final Number defaultValue;
46+
private Class<? extends Number> clazz;
47+
private boolean required;
48+
private NumberInputContext currentContext;
49+
50+
public NumberInput(Terminal terminal) {
51+
this(terminal, null);
52+
}
53+
54+
public NumberInput(Terminal terminal, String name) {
55+
this(terminal, name, null);
56+
}
57+
58+
public NumberInput(Terminal terminal, String name, Number defaultValue) {
59+
this(terminal, name, defaultValue, Integer.class);
60+
}
61+
62+
public NumberInput(Terminal terminal, String name, Number defaultValue, Class<? extends Number> clazz) {
63+
this(terminal, name, defaultValue, clazz, false);
64+
}
65+
66+
public NumberInput(Terminal terminal, String name, Number defaultValue, Class<? extends Number> clazz, boolean required) {
67+
this(terminal, name, defaultValue, clazz, required, null);
68+
}
69+
70+
public NumberInput(Terminal terminal, String name, Number defaultValue, Class<? extends Number> clazz, boolean required,
71+
Function<NumberInputContext, List<AttributedString>> renderer) {
72+
super(terminal, name, null);
73+
setRenderer(renderer != null ? renderer : new DefaultRenderer());
74+
setTemplateLocation("classpath:org/springframework/shell/component/number-input-default.stg");
75+
this.defaultValue = defaultValue;
76+
this.clazz = clazz;
77+
this.required = required;
78+
}
79+
80+
public void setNumberClass(Class<? extends Number> clazz) {
81+
this.clazz = clazz;
82+
}
83+
84+
public void setRequired(boolean required) {
85+
this.required = required;
86+
}
87+
88+
@Override
89+
public NumberInputContext getThisContext(ComponentContext<?> context) {
90+
if (context != null && currentContext == context) {
91+
return currentContext;
92+
}
93+
currentContext = NumberInputContext.of(defaultValue, clazz, required);
94+
currentContext.setName(getName());
95+
Optional.ofNullable(context).map(ComponentContext::stream)
96+
.ifPresent(entryStream -> entryStream.forEach(e -> currentContext.put(e.getKey(), e.getValue())));
97+
return currentContext;
98+
}
99+
100+
@Override
101+
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, NumberInputContext context) {
102+
String operation = bindingReader.readBinding(keyMap);
103+
log.debug("Binding read result {}", operation);
104+
if (operation == null) {
105+
return true;
106+
}
107+
String input;
108+
switch (operation) {
109+
case OPERATION_CHAR:
110+
String lastBinding = bindingReader.getLastBinding();
111+
input = context.getInput();
112+
if (input == null) {
113+
input = lastBinding;
114+
} else {
115+
input = input + lastBinding;
116+
}
117+
context.setInput(input);
118+
checkInput(input, context);
119+
break;
120+
case OPERATION_BACKSPACE:
121+
input = context.getInput();
122+
if (StringUtils.hasLength(input)) {
123+
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
124+
}
125+
context.setInput(input);
126+
checkInput(input, context);
127+
break;
128+
case OPERATION_EXIT:
129+
Number num = parseNumber(context.getInput());
130+
131+
if (num != null) {
132+
context.setResultValue(parseNumber(context.getInput()));
133+
} else if (StringUtils.hasText(context.getInput())) {
134+
printInvalidInput(context.getInput(), context);
135+
break;
136+
} else if (context.getDefaultValue() != null) {
137+
context.setResultValue(context.getDefaultValue());
138+
} else if (required) {
139+
context.setMessage("This field is mandatory", TextComponentContext.MessageLevel.ERROR);
140+
break;
141+
}
142+
return true;
143+
default:
144+
break;
145+
}
146+
return false;
147+
}
148+
149+
private Number parseNumber(String input) {
150+
if (!StringUtils.hasText(input)) {
151+
return null;
152+
}
153+
154+
try {
155+
return NumberUtils.parseNumber(input, clazz);
156+
} catch (NumberFormatException e) {
157+
return null;
158+
}
159+
}
160+
161+
private void checkInput(String input, NumberInputContext context) {
162+
if (!StringUtils.hasText(input)) {
163+
context.setMessage(null);
164+
return;
165+
}
166+
Number num = parseNumber(input);
167+
if (num == null) {
168+
printInvalidInput(input, context);
169+
}
170+
else {
171+
context.setMessage(null);
172+
}
173+
}
174+
175+
private void printInvalidInput(String input, NumberInputContext context) {
176+
String msg = String.format("Sorry, your input is invalid: '%s', try again", input);
177+
context.setMessage(msg, MessageLevel.ERROR);
178+
}
179+
180+
public interface NumberInputContext extends TextComponentContext<Number, NumberInputContext> {
181+
182+
/**
183+
* Gets a default value.
184+
*
185+
* @return a default value
186+
*/
187+
Number getDefaultValue();
188+
189+
/**
190+
* Sets a default value.
191+
*
192+
* @param defaultValue the default value
193+
*/
194+
void setDefaultValue(Number defaultValue);
195+
196+
/**
197+
* Gets a default number class.
198+
*
199+
* @return a default number class
200+
*/
201+
Class<? extends Number> getDefaultClass();
202+
203+
/**
204+
* Sets a default number class.
205+
*
206+
* @param defaultClass the default number class
207+
*/
208+
void setDefaultClass(Class<? extends Number> defaultClass);
209+
210+
/**
211+
* Sets flag for mandatory input.
212+
*
213+
* @param required true if input is required
214+
*/
215+
void setRequired(boolean required);
216+
217+
/**
218+
* Returns flag if input is required.
219+
*
220+
* @return true if input is required, false otherwise
221+
*/
222+
boolean isRequired();
223+
224+
/**
225+
* Gets an empty {@link NumberInputContext}.
226+
*
227+
* @return empty number input context
228+
*/
229+
public static NumberInputContext empty() {
230+
return of(null);
231+
}
232+
233+
/**
234+
* Gets an {@link NumberInputContext}.
235+
*
236+
* @return number input context
237+
*/
238+
public static NumberInputContext of(Number defaultValue) {
239+
return new DefaultNumberInputContext(defaultValue, Integer.class, false);
240+
}
241+
242+
/**
243+
* Gets an {@link NumberInputContext}.
244+
*
245+
* @return number input context
246+
*/
247+
public static NumberInputContext of(Number defaultValue, Class<? extends Number> defaultClass) {
248+
return new DefaultNumberInputContext(defaultValue, defaultClass, false);
249+
}
250+
251+
/**
252+
* Gets an {@link NumberInputContext}.
253+
*
254+
* @return number input context
255+
*/
256+
public static NumberInputContext of(Number defaultValue, Class<? extends Number> defaultClass, boolean required) {
257+
return new DefaultNumberInputContext(defaultValue, defaultClass, required);
258+
}
259+
}
260+
261+
private static class DefaultNumberInputContext extends BaseTextComponentContext<Number, NumberInputContext> implements NumberInputContext {
262+
263+
private Number defaultValue;
264+
private Class<? extends Number> defaultClass;
265+
private boolean required;
266+
267+
public DefaultNumberInputContext(Number defaultValue, Class<? extends Number> defaultClass, boolean required) {
268+
this.defaultValue = defaultValue;
269+
this.defaultClass = defaultClass;
270+
this.required = required;
271+
}
272+
273+
@Override
274+
public Number getDefaultValue() {
275+
return defaultValue;
276+
}
277+
278+
@Override
279+
public void setDefaultValue(Number defaultValue) {
280+
this.defaultValue = defaultValue;
281+
}
282+
283+
@Override
284+
public Class<? extends Number> getDefaultClass() {
285+
return defaultClass;
286+
}
287+
288+
@Override
289+
public void setDefaultClass(Class<? extends Number> defaultClass) {
290+
this.defaultClass = defaultClass;
291+
}
292+
293+
@Override
294+
public void setRequired(boolean required) {
295+
this.required = required;
296+
}
297+
298+
@Override
299+
public boolean isRequired() {
300+
return required;
301+
}
302+
303+
@Override
304+
public Map<String, Object> toTemplateModel() {
305+
Map<String, Object> attributes = super.toTemplateModel();
306+
attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null);
307+
attributes.put("defaultClass", getDefaultClass().getSimpleName());
308+
attributes.put("required", isRequired());
309+
Map<String, Object> model = new HashMap<>();
310+
model.put("model", attributes);
311+
return model;
312+
}
313+
}
314+
315+
private class DefaultRenderer implements Function<NumberInputContext, List<AttributedString>> {
316+
317+
@Override
318+
public List<AttributedString> apply(NumberInputContext context) {
319+
return renderTemplateResource(context.toTemplateModel());
320+
}
321+
}
322+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.component.flow;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.function.Consumer;
21+
import java.util.function.Function;
22+
23+
import org.jline.utils.AttributedString;
24+
import org.springframework.shell.component.NumberInput.NumberInputContext;
25+
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
26+
import org.springframework.shell.component.flow.ComponentFlow.Builder;
27+
28+
/**
29+
* Base impl for {@link NumberInputSpec}.
30+
*
31+
* @author Nicola Di Falco
32+
*/
33+
public abstract class BaseNumberInput extends BaseInput<NumberInputSpec> implements NumberInputSpec {
34+
35+
private String name;
36+
private Number resultValue;
37+
private ResultMode resultMode;
38+
private Number defaultValue;
39+
private Class<? extends Number> clazz = Integer.class;
40+
private boolean required = false;
41+
private Function<NumberInputContext, List<AttributedString>> renderer;
42+
private final List<Consumer<NumberInputContext>> preHandlers = new ArrayList<>();
43+
private final List<Consumer<NumberInputContext>> postHandlers = new ArrayList<>();
44+
private boolean storeResult = true;
45+
private String templateLocation;
46+
private Function<NumberInputContext, String> next;
47+
48+
protected BaseNumberInput(BaseBuilder builder, String id) {
49+
super(builder, id);
50+
}
51+
52+
@Override
53+
public NumberInputSpec name(String name) {
54+
this.name = name;
55+
return this;
56+
}
57+
58+
@Override
59+
public NumberInputSpec resultValue(Number resultValue) {
60+
this.resultValue = resultValue;
61+
return this;
62+
}
63+
64+
@Override
65+
public NumberInputSpec resultMode(ResultMode resultMode) {
66+
this.resultMode = resultMode;
67+
return this;
68+
}
69+
70+
@Override
71+
public NumberInputSpec defaultValue(Number defaultValue) {
72+
this.defaultValue = defaultValue;
73+
return this;
74+
}
75+
76+
@Override
77+
public NumberInputSpec numberClass(Class<? extends Number> clazz) {
78+
this.clazz = clazz;
79+
return this;
80+
}
81+
82+
@Override
83+
public NumberInputSpec required() {
84+
this.required = true;
85+
return this;
86+
}
87+
88+
@Override
89+
public NumberInputSpec renderer(Function<NumberInputContext, List<AttributedString>> renderer) {
90+
this.renderer = renderer;
91+
return this;
92+
}
93+
94+
@Override
95+
public NumberInputSpec template(String location) {
96+
this.templateLocation = location;
97+
return this;
98+
}
99+
100+
@Override
101+
public NumberInputSpec preHandler(Consumer<NumberInputContext> handler) {
102+
this.preHandlers.add(handler);
103+
return this;
104+
}
105+
106+
@Override
107+
public NumberInputSpec postHandler(Consumer<NumberInputContext> handler) {
108+
this.postHandlers.add(handler);
109+
return this;
110+
}
111+
112+
@Override
113+
public NumberInputSpec storeResult(boolean store) {
114+
this.storeResult = store;
115+
return this;
116+
}
117+
118+
@Override
119+
public NumberInputSpec next(Function<NumberInputContext, String> next) {
120+
this.next = next;
121+
return this;
122+
}
123+
124+
@Override
125+
public Builder and() {
126+
getBuilder().addNumberInput(this);
127+
return getBuilder();
128+
}
129+
130+
@Override
131+
public NumberInputSpec getThis() {
132+
return this;
133+
}
134+
135+
public String getName() {
136+
return name;
137+
}
138+
139+
public Number getResultValue() {
140+
return resultValue;
141+
}
142+
143+
public ResultMode getResultMode() {
144+
return resultMode;
145+
}
146+
147+
public Number getDefaultValue() {
148+
return defaultValue;
149+
}
150+
151+
public Class<? extends Number> getNumberClass() {
152+
return clazz;
153+
}
154+
155+
public boolean isRequired() {
156+
return required;
157+
}
158+
159+
public Function<NumberInputContext, List<AttributedString>> getRenderer() {
160+
return renderer;
161+
}
162+
163+
public String getTemplateLocation() {
164+
return templateLocation;
165+
}
166+
167+
public List<Consumer<NumberInputContext>> getPreHandlers() {
168+
return preHandlers;
169+
}
170+
171+
public List<Consumer<NumberInputContext>> getPostHandlers() {
172+
return postHandlers;
173+
}
174+
175+
public boolean isStoreResult() {
176+
return storeResult;
177+
}
178+
179+
public Function<NumberInputContext, String> getNext() {
180+
return next;
181+
}
182+
}

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

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,27 @@
2525
import java.util.concurrent.atomic.AtomicInteger;
2626
import java.util.function.Consumer;
2727
import java.util.function.Function;
28+
import java.util.function.UnaryOperator;
2829
import java.util.stream.Collectors;
2930
import java.util.stream.Stream;
3031

3132
import org.jline.terminal.Terminal;
3233
import org.slf4j.Logger;
3334
import org.slf4j.LoggerFactory;
34-
3535
import org.springframework.core.OrderComparator;
3636
import org.springframework.core.Ordered;
3737
import org.springframework.core.io.ResourceLoader;
3838
import org.springframework.shell.component.ConfirmationInput;
39+
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
3940
import org.springframework.shell.component.MultiItemSelector;
4041
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
42+
import org.springframework.shell.component.NumberInput;
43+
import org.springframework.shell.component.NumberInput.NumberInputContext;
4144
import org.springframework.shell.component.PathInput;
4245
import org.springframework.shell.component.PathInput.PathInputContext;
4346
import org.springframework.shell.component.SingleItemSelector;
4447
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
4548
import org.springframework.shell.component.StringInput;
46-
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
4749
import org.springframework.shell.component.StringInput.StringInputContext;
4850
import org.springframework.shell.component.context.ComponentContext;
4951
import org.springframework.shell.component.support.SelectorItem;
@@ -102,6 +104,14 @@ interface Builder {
102104
*/
103105
StringInputSpec withStringInput(String id);
104106

107+
/**
108+
* Gets a builder for number input.
109+
*
110+
* @param id the identifier
111+
* @return builder for number input
112+
*/
113+
NumberInputSpec withNumberInput(String id);
114+
105115
/**
106116
* Gets a builder for path input.
107117
*
@@ -183,6 +193,7 @@ interface Builder {
183193
static abstract class BaseBuilder implements Builder {
184194

185195
private final List<BaseStringInput> stringInputs = new ArrayList<>();
196+
private final List<BaseNumberInput> numberInputs = new ArrayList<>();
186197
private final List<BasePathInput> pathInputs = new ArrayList<>();
187198
private final List<BaseConfirmationInput> confirmationInputs = new ArrayList<>();
188199
private final List<BaseSingleItemSelector> singleItemSelectors = new ArrayList<>();
@@ -198,7 +209,7 @@ static abstract class BaseBuilder implements Builder {
198209

199210
@Override
200211
public ComponentFlow build() {
201-
return new DefaultComponentFlow(terminal, resourceLoader, templateExecutor, stringInputs, pathInputs,
212+
return new DefaultComponentFlow(terminal, resourceLoader, templateExecutor, stringInputs, numberInputs, pathInputs,
202213
confirmationInputs, singleItemSelectors, multiItemSelectors);
203214
}
204215

@@ -207,6 +218,11 @@ public StringInputSpec withStringInput(String id) {
207218
return new DefaultStringInputSpec(this, id);
208219
}
209220

221+
@Override
222+
public NumberInputSpec withNumberInput(String id) {
223+
return new DefaultNumberInputSpec(this, id);
224+
}
225+
210226
@Override
211227
public PathInputSpec withPathInput(String id) {
212228
return new DefaultPathInputSpec(this, id);
@@ -253,6 +269,7 @@ public Builder clone() {
253269
@Override
254270
public Builder reset() {
255271
stringInputs.clear();
272+
numberInputs.clear();
256273
pathInputs.clear();
257274
confirmationInputs.clear();
258275
singleItemSelectors.clear();
@@ -268,6 +285,12 @@ void addStringInput(BaseStringInput input) {
268285
stringInputs.add(input);
269286
}
270287

288+
void addNumberInput(BaseNumberInput input) {
289+
checkUniqueId(input.getId());
290+
input.setOrder(order.getAndIncrement());
291+
numberInputs.add(input);
292+
}
293+
271294
void addPathInput(BasePathInput input) {
272295
checkUniqueId(input.getId());
273296
input.setOrder(order.getAndIncrement());
@@ -343,6 +366,7 @@ static class DefaultComponentFlow implements ComponentFlow {
343366
private static final Logger log = LoggerFactory.getLogger(DefaultComponentFlow.class);
344367
private final Terminal terminal;
345368
private final List<BaseStringInput> stringInputs;
369+
private final List<BaseNumberInput> numberInputs;
346370
private final List<BasePathInput> pathInputs;
347371
private final List<BaseConfirmationInput> confirmationInputs;
348372
private final List<BaseSingleItemSelector> singleInputs;
@@ -351,12 +375,14 @@ static class DefaultComponentFlow implements ComponentFlow {
351375
private final TemplateExecutor templateExecutor;
352376

353377
DefaultComponentFlow(Terminal terminal, ResourceLoader resourceLoader, TemplateExecutor templateExecutor,
354-
List<BaseStringInput> stringInputs, List<BasePathInput> pathInputs, List<BaseConfirmationInput> confirmationInputs,
355-
List<BaseSingleItemSelector> singleInputs, List<BaseMultiItemSelector> multiInputs) {
378+
List<BaseStringInput> stringInputs, List<BaseNumberInput> numberInputs, List<BasePathInput> pathInputs,
379+
List<BaseConfirmationInput> confirmationInputs, List<BaseSingleItemSelector> singleInputs,
380+
List<BaseMultiItemSelector> multiInputs) {
356381
this.terminal = terminal;
357382
this.resourceLoader = resourceLoader;
358383
this.templateExecutor = templateExecutor;
359384
this.stringInputs = stringInputs;
385+
this.numberInputs = numberInputs;
360386
this.pathInputs = pathInputs;
361387
this.confirmationInputs = confirmationInputs;
362388
this.singleInputs = singleInputs;
@@ -407,7 +433,7 @@ static class Node {
407433

408434
private DefaultComponentFlowResult runGetResults() {
409435
List<OrderedInputOperation> oios = Stream
410-
.of(stringInputsStream(), pathInputsStream(), confirmationInputsStream(),
436+
.of(stringInputsStream(), numberInputsStream(), pathInputsStream(), confirmationInputsStream(),
411437
singleItemSelectorsStream(), multiItemSelectorsStream())
412438
.flatMap(oio -> oio)
413439
.sorted(OrderComparator.INSTANCE)
@@ -487,6 +513,49 @@ private Stream<OrderedInputOperation> stringInputsStream() {
487513
});
488514
}
489515

516+
private Stream<OrderedInputOperation> numberInputsStream() {
517+
return numberInputs.stream().map(input -> {
518+
NumberInput selector = new NumberInput(terminal, input.getName(), input.getDefaultValue(), input.getNumberClass(), input.isRequired());
519+
UnaryOperator<ComponentContext<?>> operation = context -> {
520+
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
521+
&& input.getResultValue() != null) {
522+
context.put(input.getId(), input.getResultValue());
523+
return context;
524+
}
525+
selector.setResourceLoader(resourceLoader);
526+
selector.setTemplateExecutor(templateExecutor);
527+
selector.setNumberClass(input.getNumberClass());
528+
if (StringUtils.hasText(input.getTemplateLocation())) {
529+
selector.setTemplateLocation(input.getTemplateLocation());
530+
}
531+
if (input.getRenderer() != null) {
532+
selector.setRenderer(input.getRenderer());
533+
}
534+
if (input.isStoreResult()) {
535+
if (input.getResultMode() == ResultMode.VERIFY && input.getResultValue() != null) {
536+
selector.addPreRunHandler(c -> {
537+
c.setDefaultValue(input.getResultValue());
538+
c.setRequired(input.isRequired());
539+
});
540+
}
541+
selector.addPostRunHandler(c -> c.put(input.getId(), c.getResultValue()));
542+
}
543+
for (Consumer<NumberInputContext> handler : input.getPreHandlers()) {
544+
selector.addPreRunHandler(handler);
545+
}
546+
for (Consumer<NumberInputContext> handler : input.getPostHandlers()) {
547+
selector.addPostRunHandler(handler);
548+
}
549+
return selector.run(context);
550+
};
551+
Function<NumberInputContext, String> f1 = input.getNext();
552+
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
553+
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
554+
: null;
555+
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
556+
});
557+
}
558+
490559
private Stream<OrderedInputOperation> pathInputsStream() {
491560
return pathInputs.stream().map(input -> {
492561
PathInput selector = new PathInput(terminal, input.getName());
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.component.flow;
17+
18+
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
19+
20+
/**
21+
* Default impl for {@link BaseNumberInput}.
22+
*
23+
* @author Nicola Di Falco
24+
*/
25+
public class DefaultNumberInputSpec extends BaseNumberInput {
26+
27+
public DefaultNumberInputSpec(BaseBuilder builder, String id) {
28+
super(builder, id);
29+
}
30+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.component.flow;
17+
18+
import java.util.List;
19+
import java.util.function.Consumer;
20+
import java.util.function.Function;
21+
22+
import org.jline.utils.AttributedString;
23+
import org.springframework.shell.component.NumberInput.NumberInputContext;
24+
import org.springframework.shell.component.context.ComponentContext;
25+
import org.springframework.shell.component.flow.ComponentFlow.Builder;
26+
27+
/**
28+
* Interface for number input spec builder.
29+
*
30+
* @author Nicola Di Falco
31+
*/
32+
public interface NumberInputSpec extends BaseInputSpec<NumberInputSpec> {
33+
34+
/**
35+
* Sets a name.
36+
*
37+
* @param name the name
38+
* @return a builder
39+
*/
40+
NumberInputSpec name(String name);
41+
42+
/**
43+
* Sets a result value.
44+
*
45+
* @param resultValue the result value
46+
* @return a builder
47+
*/
48+
NumberInputSpec resultValue(Number resultValue);
49+
50+
/**
51+
* Sets a result mode.
52+
*
53+
* @param resultMode the result mode
54+
* @return a builder
55+
*/
56+
NumberInputSpec resultMode(ResultMode resultMode);
57+
58+
/**
59+
* Sets a default value.
60+
*
61+
* @param defaultValue the defult value
62+
* @return a builder
63+
*/
64+
NumberInputSpec defaultValue(Number defaultValue);
65+
66+
/**
67+
* Sets the class of the number. Defaults to Integer.
68+
*
69+
* @param clazz the specific number class
70+
* @return a builder
71+
*/
72+
NumberInputSpec numberClass(Class<? extends Number> clazz);
73+
74+
/**
75+
* Sets input to required
76+
*
77+
* @return a builder
78+
*/
79+
NumberInputSpec required();
80+
81+
/**
82+
* Sets a renderer function.
83+
*
84+
* @param renderer the renderer
85+
* @return a builder
86+
*/
87+
NumberInputSpec renderer(Function<NumberInputContext, List<AttributedString>> renderer);
88+
89+
/**
90+
* Sets a default renderer template location.
91+
*
92+
* @param location the template location
93+
* @return a builder
94+
*/
95+
NumberInputSpec template(String location);
96+
97+
/**
98+
* Adds a pre-run context handler.
99+
*
100+
* @param handler the context handler
101+
* @return a builder
102+
*/
103+
NumberInputSpec preHandler(Consumer<NumberInputContext> handler);
104+
105+
/**
106+
* Adds a post-run context handler.
107+
*
108+
* @param handler the context handler
109+
* @return a builder
110+
*/
111+
NumberInputSpec postHandler(Consumer<NumberInputContext> handler);
112+
113+
/**
114+
* Automatically stores result from a {@link NumberInputContext} into
115+
* {@link ComponentContext} with key given to builder. Defaults to {@code true}.
116+
*
117+
* @param store the flag if storing result
118+
* @return a builder
119+
*/
120+
NumberInputSpec storeResult(boolean store);
121+
122+
/**
123+
* Define a function which may return id of a next component to go. Returning a
124+
* {@code null} or non existent id indicates that flow should stop.
125+
*
126+
* @param next next component function
127+
* @return a builder
128+
*/
129+
NumberInputSpec next(Function<NumberInputContext, String> next);
130+
131+
/**
132+
* Build and return parent builder.
133+
*
134+
* @return the parent builder
135+
*/
136+
Builder and();
137+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
12+
// info section after '? xxx'
13+
info(model) ::= <%
14+
<if(model.input)>
15+
<model.input>
16+
<else>
17+
<if(model.required)>
18+
<("[Required]"); format="style-value">
19+
<endif>
20+
<("[Number Type: "); format="style-value"><model.defaultClass; format="style-value"><("]"); format="style-value">
21+
<if(model.defaultValue)>
22+
<("[Default "); format="style-value"><model.defaultValue; format="style-value"><("]"); format="style-value">
23+
<endif>
24+
<endif>
25+
%>
26+
27+
// start '? xxx' shows both running and result
28+
question_name(model) ::= <<
29+
<({<figures.questionMark>}); format="style-list-value"> <model.name; format="style-title">
30+
>>
31+
32+
// component result
33+
result(model) ::= <<
34+
<question_name(model)> <model.resultValue; format="style-value">
35+
>>
36+
37+
// component is running
38+
running(model) ::= <<
39+
<question_name(model)> <info(model)>
40+
<message(model)>
41+
>>
42+
43+
// main
44+
main(model) ::= <<
45+
<if(model.resultValue)><result(model)><else><running(model)><endif>
46+
>>
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.component;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.io.ByteArrayInputStream;
21+
import java.io.ByteArrayOutputStream;
22+
import java.io.IOException;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.concurrent.CountDownLatch;
25+
import java.util.concurrent.ExecutorService;
26+
import java.util.concurrent.Executors;
27+
import java.util.concurrent.TimeUnit;
28+
import java.util.concurrent.atomic.AtomicReference;
29+
30+
import org.jline.terminal.impl.DumbTerminal;
31+
import org.junit.jupiter.api.AfterEach;
32+
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.Test;
34+
import org.springframework.core.io.DefaultResourceLoader;
35+
import org.springframework.shell.component.NumberInput.NumberInputContext;
36+
import org.springframework.shell.component.context.ComponentContext;
37+
38+
public class NumberInputTests extends AbstractShellTests {
39+
40+
private ExecutorService service;
41+
private CountDownLatch latch1;
42+
private CountDownLatch latch2;
43+
private AtomicReference<NumberInputContext> result1;
44+
private AtomicReference<NumberInputContext> result2;
45+
46+
@BeforeEach
47+
public void setupTests() {
48+
service = Executors.newFixedThreadPool(1);
49+
latch1 = new CountDownLatch(1);
50+
latch2 = new CountDownLatch(1);
51+
result1 = new AtomicReference<>();
52+
result2 = new AtomicReference<>();
53+
}
54+
55+
@AfterEach
56+
public void cleanupTests() {
57+
latch1 = null;
58+
latch2 = null;
59+
result1 = null;
60+
result2 = null;
61+
if (service != null) {
62+
service.shutdown();
63+
}
64+
service = null;
65+
}
66+
67+
@Test
68+
void testNoTty() throws Exception {
69+
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
70+
ByteArrayOutputStream out = new ByteArrayOutputStream();
71+
DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8);
72+
73+
ComponentContext<?> empty = ComponentContext.empty();
74+
NumberInput component1 = new NumberInput(dumbTerminal, "component1", 100, Double.class);
75+
component1.setPrintResults(true);
76+
component1.setResourceLoader(new DefaultResourceLoader());
77+
component1.setTemplateExecutor(getTemplateExecutor());
78+
79+
service.execute(() -> {
80+
NumberInputContext run1Context = component1.run(empty);
81+
result1.set(run1Context);
82+
latch1.countDown();
83+
});
84+
85+
TestBuffer testBuffer = new TestBuffer().cr();
86+
write(testBuffer.getBytes());
87+
88+
latch1.await(2, TimeUnit.SECONDS);
89+
NumberInputContext run1Context = result1.get();
90+
91+
assertThat(run1Context).isNotNull();
92+
assertThat(run1Context.getResultValue()).isNull();
93+
}
94+
95+
@Test
96+
public void testResultBasic() throws InterruptedException {
97+
ComponentContext<?> empty = ComponentContext.empty();
98+
NumberInput component1 = new NumberInput(getTerminal(), "component1", 100);
99+
component1.setPrintResults(true);
100+
component1.setResourceLoader(new DefaultResourceLoader());
101+
component1.setTemplateExecutor(getTemplateExecutor());
102+
103+
service.execute(() -> {
104+
NumberInputContext run1Context = component1.run(empty);
105+
result1.set(run1Context);
106+
latch1.countDown();
107+
});
108+
109+
TestBuffer testBuffer = new TestBuffer().cr();
110+
write(testBuffer.getBytes());
111+
112+
latch1.await(2, TimeUnit.SECONDS);
113+
NumberInputContext run1Context = result1.get();
114+
115+
assertThat(run1Context).isNotNull();
116+
assertThat(run1Context.getResultValue()).isEqualTo(100);
117+
assertThat(consoleOut()).contains("component1 100");
118+
}
119+
120+
@Test
121+
public void testResultBasicWithType() throws InterruptedException {
122+
ComponentContext<?> empty = ComponentContext.empty();
123+
NumberInput component1 = new NumberInput(getTerminal(), "component1", 50.1, Float.class);
124+
component1.setPrintResults(true);
125+
component1.setResourceLoader(new DefaultResourceLoader());
126+
component1.setTemplateExecutor(getTemplateExecutor());
127+
128+
service.execute(() -> {
129+
NumberInputContext run1Context = component1.run(empty);
130+
result1.set(run1Context);
131+
latch1.countDown();
132+
});
133+
134+
TestBuffer testBuffer = new TestBuffer().cr();
135+
write(testBuffer.getBytes());
136+
137+
latch1.await(2, TimeUnit.SECONDS);
138+
NumberInputContext run1Context = result1.get();
139+
140+
assertThat(run1Context).isNotNull();
141+
assertThat(run1Context.getResultValue()).isEqualTo(50.1);
142+
assertThat(consoleOut()).contains("component1 50.1");
143+
}
144+
145+
@Test
146+
public void testResultUserInput() throws InterruptedException {
147+
ComponentContext<?> empty = ComponentContext.empty();
148+
NumberInput component1 = new NumberInput(getTerminal(), "component1");
149+
component1.setNumberClass(Double.class);
150+
component1.setResourceLoader(new DefaultResourceLoader());
151+
component1.setTemplateExecutor(getTemplateExecutor());
152+
153+
service.execute(() -> {
154+
NumberInputContext run1Context = component1.run(empty);
155+
result1.set(run1Context);
156+
latch1.countDown();
157+
});
158+
159+
TestBuffer testBuffer = new TestBuffer().append("123.3").cr();
160+
write(testBuffer.getBytes());
161+
162+
latch1.await(2, TimeUnit.SECONDS);
163+
NumberInputContext run1Context = result1.get();
164+
165+
assertThat(run1Context).isNotNull();
166+
assertThat(run1Context.getResultValue()).isEqualTo(123.3d);
167+
}
168+
169+
@Test
170+
public void testPassingViaContext() throws InterruptedException {
171+
ComponentContext<?> empty = ComponentContext.empty();
172+
NumberInput component1 = new NumberInput(getTerminal(), "component1", 1);
173+
NumberInput component2 = new NumberInput(getTerminal(), "component2", 2);
174+
component1.setResourceLoader(new DefaultResourceLoader());
175+
component1.setTemplateExecutor(getTemplateExecutor());
176+
component2.setResourceLoader(new DefaultResourceLoader());
177+
component2.setTemplateExecutor(getTemplateExecutor());
178+
179+
component1.addPostRunHandler(context -> {
180+
context.put(1, context.getResultValue());
181+
});
182+
183+
component2.addPreRunHandler(context -> {
184+
Integer component1ResultValue = context.get(1);
185+
context.setDefaultValue(component1ResultValue);
186+
});
187+
component2.addPostRunHandler(context -> {
188+
});
189+
190+
service.execute(() -> {
191+
NumberInputContext run1Context = component1.run(empty);
192+
result1.set(run1Context);
193+
latch1.countDown();
194+
});
195+
196+
TestBuffer testBuffer = new TestBuffer().cr();
197+
write(testBuffer.getBytes());
198+
199+
latch1.await(2, TimeUnit.SECONDS);
200+
201+
service.execute(() -> {
202+
NumberInputContext run1Context = result1.get();
203+
NumberInputContext run2Context = component2.run(run1Context);
204+
result2.set(run2Context);
205+
latch2.countDown();
206+
});
207+
208+
write(testBuffer.getBytes());
209+
210+
latch2.await(2, TimeUnit.SECONDS);
211+
212+
NumberInputContext run1Context = result1.get();
213+
NumberInputContext run2Context = result2.get();
214+
215+
assertThat(run1Context).isNotSameAs(run2Context);
216+
217+
assertThat(run1Context).isNotNull();
218+
assertThat(run2Context).isNotNull();
219+
assertThat(run1Context.getResultValue()).isEqualTo(1);
220+
assertThat(run2Context.getResultValue()).isEqualTo(1);
221+
}
222+
223+
@Test
224+
public void testResultUserInputInvalidInput() throws InterruptedException, IOException {
225+
ComponentContext<?> empty = ComponentContext.empty();
226+
NumberInput component1 = new NumberInput(getTerminal(), "component1");
227+
component1.setResourceLoader(new DefaultResourceLoader());
228+
component1.setTemplateExecutor(getTemplateExecutor());
229+
230+
service.execute(() -> {
231+
NumberInputContext run1Context = component1.run(empty);
232+
result1.set(run1Context);
233+
latch1.countDown();
234+
});
235+
236+
TestBuffer testBuffer = new TestBuffer().append("x").cr();
237+
write(testBuffer.getBytes());
238+
239+
latch1.await(2, TimeUnit.SECONDS);
240+
241+
NumberInputContext run1Context = result1.get();
242+
assertThat(consoleOut()).contains("input is invalid");
243+
assertThat(run1Context).isNull();
244+
245+
// backspace 2 : cr + input
246+
testBuffer.backspace(2).append("2").cr();
247+
write(testBuffer.getBytes());
248+
249+
latch1.await(2, TimeUnit.SECONDS);
250+
251+
run1Context = result1.get();
252+
assertThat(run1Context).isNotNull();
253+
assertThat(run1Context.getResultValue()).isEqualTo(2);
254+
}
255+
256+
@Test
257+
public void testResultMandatoryInput() throws InterruptedException {
258+
ComponentContext<?> empty = ComponentContext.empty();
259+
NumberInput component1 = new NumberInput(getTerminal());
260+
component1.setResourceLoader(new DefaultResourceLoader());
261+
component1.setTemplateExecutor(getTemplateExecutor());
262+
component1.setRequired(true);
263+
264+
service.execute(() -> {
265+
NumberInputContext run1Context = component1.run(empty);
266+
result1.set(run1Context);
267+
latch1.countDown();
268+
});
269+
270+
TestBuffer testBuffer = new TestBuffer().cr();
271+
write(testBuffer.getBytes());
272+
273+
latch1.await(2, TimeUnit.SECONDS);
274+
275+
NumberInputContext run1Context = result1.get();
276+
assertThat(consoleOut()).contains("This field is mandatory");
277+
assertThat(run1Context).isNull();
278+
279+
testBuffer.append("2").cr();
280+
write(testBuffer.getBytes());
281+
282+
latch1.await(2, TimeUnit.SECONDS);
283+
run1Context = result1.get();
284+
285+
assertThat(run1Context).isNotNull();
286+
assertThat(run1Context.getResultValue()).isEqualTo(2);
287+
}
288+
}

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.shell.component.flow;
1717

18+
import static org.assertj.core.api.Assertions.assertThat;
19+
1820
import java.nio.file.Path;
1921
import java.util.Arrays;
2022
import java.util.HashMap;
@@ -32,8 +34,6 @@
3234
import org.springframework.shell.component.flow.ComponentFlow.ComponentFlowResult;
3335
import org.springframework.test.util.ReflectionTestUtils;
3436

35-
import static org.assertj.core.api.Assertions.assertThat;
36-
3737
public class ComponentFlowTests extends AbstractShellTests {
3838

3939
@Test
@@ -54,6 +54,18 @@ public void testSimpleFlow() throws InterruptedException {
5454
.withStringInput("field2")
5555
.name("Field2")
5656
.and()
57+
.withNumberInput("number1")
58+
.name("Number1")
59+
.and()
60+
.withNumberInput("number2")
61+
.name("Number2")
62+
.defaultValue(20.5)
63+
.numberClass(Double.class)
64+
.and()
65+
.withNumberInput("number3")
66+
.name("Number3")
67+
.required()
68+
.and()
5769
.withPathInput("path1")
5870
.name("Path1")
5971
.and()
@@ -82,6 +94,15 @@ public void testSimpleFlow() throws InterruptedException {
8294
// field2
8395
testBuffer = new TestBuffer().append("Field2Value").cr();
8496
write(testBuffer.getBytes());
97+
// number1
98+
testBuffer = new TestBuffer().append("35").cr();
99+
write(testBuffer.getBytes());
100+
// number2
101+
testBuffer = new TestBuffer().cr();
102+
write(testBuffer.getBytes());
103+
// number3
104+
testBuffer = new TestBuffer().cr().append("5").cr();
105+
write(testBuffer.getBytes());
85106
// path1
86107
testBuffer = new TestBuffer().append("fakedir").cr();
87108
write(testBuffer.getBytes());
@@ -97,11 +118,17 @@ public void testSimpleFlow() throws InterruptedException {
97118
assertThat(inputWizardResult).isNotNull();
98119
String field1 = inputWizardResult.getContext().get("field1");
99120
String field2 = inputWizardResult.getContext().get("field2");
121+
Integer number1 = inputWizardResult.getContext().get("number1");
122+
Double number2 = inputWizardResult.getContext().get("number2");
123+
Integer number3 = inputWizardResult.getContext().get("number3");
100124
Path path1 = inputWizardResult.getContext().get("path1");
101125
String single1 = inputWizardResult.getContext().get("single1");
102126
List<String> multi1 = inputWizardResult.getContext().get("multi1");
103127
assertThat(field1).isEqualTo("defaultField1Value");
104128
assertThat(field2).isEqualTo("Field2Value");
129+
assertThat(number1).isEqualTo(35);
130+
assertThat(number2).isEqualTo(20.5);
131+
assertThat(number3).isEqualTo(5);
105132
assertThat(path1.toString()).contains("fakedir");
106133
assertThat(single1).isEqualTo("value1");
107134
assertThat(multi1).containsExactlyInAnyOrder("value2");
@@ -134,6 +161,10 @@ public void testSkipsGivenComponents() throws InterruptedException {
134161
.resultValue(false)
135162
.resultMode(ResultMode.ACCEPT)
136163
.and()
164+
.withNumberInput("id6")
165+
.resultValue(50)
166+
.resultMode(ResultMode.ACCEPT)
167+
.and()
137168
.build();
138169

139170
ExecutorService service = Executors.newFixedThreadPool(1);
@@ -154,12 +185,14 @@ public void testSkipsGivenComponents() throws InterruptedException {
154185
String id3 = inputWizardResult.getContext().get("id3");
155186
List<String> id4 = inputWizardResult.getContext().get("id4");
156187
Boolean id5 = inputWizardResult.getContext().get("id5");
188+
Integer id6 = inputWizardResult.getContext().get("id6");
157189

158190
assertThat(id1).isEqualTo("value1");
159191
assertThat(id2.toString()).contains("value2");
160192
assertThat(id3).isEqualTo("value3");
161193
assertThat(id4).containsExactlyInAnyOrder("value4");
162194
assertThat(id5).isFalse();
195+
assertThat(id6).isEqualTo(50);
163196
}
164197

165198
@Test

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424

2525
import org.jline.utils.AttributedString;
2626
import org.jline.utils.AttributedStringBuilder;
27-
2827
import org.springframework.shell.component.ConfirmationInput;
2928
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
3029
import org.springframework.shell.component.MultiItemSelector;
3130
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
31+
import org.springframework.shell.component.NumberInput;
32+
import org.springframework.shell.component.NumberInput.NumberInputContext;
3233
import org.springframework.shell.component.PathInput;
3334
import org.springframework.shell.component.PathSearch;
3435
import org.springframework.shell.component.PathInput.PathInputContext;
@@ -60,6 +61,34 @@ public String stringInput(boolean mask) {
6061
return "Got value " + context.getResultValue();
6162
}
6263

64+
@ShellMethod(key = "component number", value = "Number input", group = "Components")
65+
public String numberInput(Number defaultValue) {
66+
NumberInput component = new NumberInput(getTerminal(), "Enter value", defaultValue);
67+
component.setResourceLoader(getResourceLoader());
68+
component.setTemplateExecutor(getTemplateExecutor());
69+
NumberInputContext context = component.run(NumberInputContext.empty());
70+
return "Got value " + context.getResultValue();
71+
}
72+
73+
@ShellMethod(key = "component number double", value = "Number double input", group = "Components")
74+
public String numberInputDouble() {
75+
NumberInput component = new NumberInput(getTerminal(), "Enter value", 99.9, Double.class);
76+
component.setResourceLoader(getResourceLoader());
77+
component.setTemplateExecutor(getTemplateExecutor());
78+
NumberInputContext context = component.run(NumberInputContext.empty());
79+
return "Got value " + context.getResultValue();
80+
}
81+
82+
@ShellMethod(key = "component number required", value = "Number input", group = "Components")
83+
public String numberInputRequired() {
84+
NumberInput component = new NumberInput(getTerminal(), "Enter value");
85+
component.setRequired(true);
86+
component.setResourceLoader(getResourceLoader());
87+
component.setTemplateExecutor(getTemplateExecutor());
88+
NumberInputContext context = component.run(NumberInputContext.empty());
89+
return "Got value " + context.getResultValue();
90+
}
91+
6392
@ShellMethod(key = "component path input", value = "Path input", group = "Components")
6493
public String pathInput() {
6594
PathInput component = new PathInput(getTerminal(), "Enter value");

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.util.stream.IntStream;
2525

2626
import org.jline.terminal.impl.DumbTerminal;
27-
2827
import org.springframework.beans.factory.annotation.Autowired;
2928
import org.springframework.context.annotation.Bean;
3029
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
@@ -62,6 +61,18 @@ public void showcase1() {
6261
.withStringInput("field2")
6362
.name("Field2")
6463
.and()
64+
.withNumberInput("number1")
65+
.name("Number1")
66+
.and()
67+
.withNumberInput("number2")
68+
.name("Number2")
69+
.defaultValue(20.5)
70+
.numberClass(Double.class)
71+
.and()
72+
.withNumberInput("number3")
73+
.name("Field3")
74+
.required()
75+
.and()
6576
.withConfirmationInput("confirmation1")
6677
.name("Confirmation1")
6778
.and()
@@ -84,6 +95,8 @@ public void showcase1() {
8495
public String showcase2(
8596
@ShellOption(help = "Field1 value", defaultValue = ShellOption.NULL) String field1,
8697
@ShellOption(help = "Field2 value", defaultValue = ShellOption.NULL) String field2,
98+
@ShellOption(help = "Number1 value", defaultValue = ShellOption.NULL) Integer number1,
99+
@ShellOption(help = "Number2 value", defaultValue = ShellOption.NULL) Double number2,
87100
@ShellOption(help = "Confirmation1 value", defaultValue = ShellOption.NULL) Boolean confirmation1,
88101
@ShellOption(help = "Path1 value", defaultValue = ShellOption.NULL) String path1,
89102
@ShellOption(help = "Single1 value", defaultValue = ShellOption.NULL) String single1,
@@ -107,6 +120,17 @@ public String showcase2(
107120
.resultValue(field2)
108121
.resultMode(ResultMode.ACCEPT)
109122
.and()
123+
.withNumberInput("number1")
124+
.name("Number1")
125+
.resultValue(number1)
126+
.resultMode(ResultMode.ACCEPT)
127+
.and()
128+
.withNumberInput("number2")
129+
.name("Number2")
130+
.resultValue(number2)
131+
.numberClass(Double.class)
132+
.resultMode(ResultMode.ACCEPT)
133+
.and()
110134
.withConfirmationInput("confirmation1")
111135
.name("Confirmation1")
112136
.resultValue(confirmation1)
@@ -152,6 +176,9 @@ public CommandRegistration showcaseRegistration() {
152176
.withOption()
153177
.longNames("field2")
154178
.and()
179+
.withOption()
180+
.longNames("number1")
181+
.and()
155182
.withOption()
156183
.longNames("confirmation1")
157184
.type(Boolean.class)
@@ -170,6 +197,7 @@ public CommandRegistration showcaseRegistration() {
170197

171198
String field1 = ctx.getOptionValue("field1");
172199
String field2 = ctx.getOptionValue("field2");
200+
Integer number1 = ctx.getOptionValue("number1");
173201
Boolean confirmation1 = ctx.getOptionValue("confirmation1");
174202
String path1 = ctx.getOptionValue("path1");
175203
String single1 = ctx.getOptionValue("single1");
@@ -196,6 +224,11 @@ public CommandRegistration showcaseRegistration() {
196224
.resultValue(field2)
197225
.resultMode(ResultMode.ACCEPT)
198226
.and()
227+
.withNumberInput("number1")
228+
.name("Number1")
229+
.resultValue(number1)
230+
.resultMode(ResultMode.ACCEPT)
231+
.and()
199232
.withConfirmationInput("confirmation1")
200233
.name("Confirmation1")
201234
.resultValue(confirmation1)

0 commit comments

Comments
 (0)
Please sign in to comment.