Skip to content

Commit 7c4b7f3

Browse files
Nico-DFfmbenhassine
authored andcommitted
Add number input type
Resolves #849 Signed-off-by: Nicola Di Falco <[email protected]>
1 parent dba383b commit 7c4b7f3

File tree

13 files changed

+1255
-6
lines changed

13 files changed

+1255
-6
lines changed
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
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.core.tui.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.jspecify.annotations.Nullable;
29+
import org.apache.commons.logging.Log;
30+
import org.apache.commons.logging.LogFactory;
31+
32+
import org.springframework.shell.core.tui.component.NumberInput.NumberInputContext;
33+
import org.springframework.shell.core.tui.component.context.ComponentContext;
34+
import org.springframework.shell.core.tui.component.support.AbstractTextComponent;
35+
import org.springframework.shell.core.tui.component.support.AbstractTextComponent.TextComponentContext.MessageLevel;
36+
import org.springframework.util.NumberUtils;
37+
import org.springframework.util.StringUtils;
38+
39+
/**
40+
* Component for a number input.
41+
*
42+
* @author Nicola Di Falco
43+
*/
44+
public class NumberInput extends AbstractTextComponent<Number, NumberInputContext> {
45+
46+
private final static Log log = LogFactory.getLog(NumberInput.class);
47+
48+
private final @Nullable Number defaultValue;
49+
50+
private @Nullable NumberInputContext currentContext;
51+
52+
private Class<? extends Number> clazz;
53+
54+
private boolean required;
55+
56+
public NumberInput(Terminal terminal) {
57+
this(terminal, null);
58+
}
59+
60+
public NumberInput(Terminal terminal, @Nullable String name) {
61+
this(terminal, name, null);
62+
}
63+
64+
public NumberInput(Terminal terminal, @Nullable String name, @Nullable Number defaultValue) {
65+
this(terminal, name, defaultValue, Integer.class);
66+
}
67+
68+
public NumberInput(Terminal terminal, @Nullable String name, @Nullable Number defaultValue,
69+
Class<? extends Number> clazz) {
70+
this(terminal, name, defaultValue, clazz, false);
71+
}
72+
73+
public NumberInput(Terminal terminal, @Nullable String name, @Nullable Number defaultValue,
74+
Class<? extends Number> clazz, boolean required) {
75+
this(terminal, name, defaultValue, clazz, required, null);
76+
}
77+
78+
public NumberInput(Terminal terminal, @Nullable String name, @Nullable Number defaultValue,
79+
Class<? extends Number> clazz, boolean required,
80+
@Nullable Function<NumberInputContext, List<AttributedString>> renderer) {
81+
super(terminal, name, null);
82+
setRenderer(renderer != null ? renderer : new DefaultRenderer());
83+
setTemplateLocation("classpath:org/springframework/shell/component/number-input-default.stg");
84+
this.defaultValue = defaultValue;
85+
this.clazz = clazz;
86+
this.required = required;
87+
}
88+
89+
public void setNumberClass(Class<? extends Number> clazz) {
90+
this.clazz = clazz;
91+
}
92+
93+
public void setRequired(boolean required) {
94+
this.required = required;
95+
}
96+
97+
@Override
98+
public NumberInputContext getThisContext(@Nullable ComponentContext<?> context) {
99+
if (context != null && currentContext == context) {
100+
return currentContext;
101+
}
102+
currentContext = NumberInputContext.of(defaultValue, clazz, required);
103+
currentContext.setName(getName());
104+
Optional.ofNullable(context)
105+
.map(ComponentContext::stream)
106+
.ifPresent(entryStream -> entryStream.forEach(e -> currentContext.put(e.getKey(), e.getValue())));
107+
return currentContext;
108+
}
109+
110+
@Override
111+
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, NumberInputContext context) {
112+
String operation = bindingReader.readBinding(keyMap);
113+
log.debug("Binding read result " + operation);
114+
if (operation == null) {
115+
return true;
116+
}
117+
String input;
118+
switch (operation) {
119+
case OPERATION_CHAR:
120+
String lastBinding = bindingReader.getLastBinding();
121+
input = context.getInput();
122+
if (input == null) {
123+
input = lastBinding;
124+
}
125+
else {
126+
input = input + lastBinding;
127+
}
128+
context.setInput(input);
129+
checkInput(input, context);
130+
break;
131+
case OPERATION_BACKSPACE:
132+
input = context.getInput();
133+
if (StringUtils.hasLength(input)) {
134+
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
135+
}
136+
context.setInput(input);
137+
checkInput(input, context);
138+
break;
139+
case OPERATION_EXIT:
140+
Number num = parseNumber(context.getInput());
141+
142+
if (num != null) {
143+
context.setResultValue(parseNumber(context.getInput()));
144+
}
145+
else if (StringUtils.hasText(context.getInput())) {
146+
printInvalidInput(context.getInput(), context);
147+
break;
148+
}
149+
else if (context.getDefaultValue() != null) {
150+
context.setResultValue(context.getDefaultValue());
151+
}
152+
else if (required) {
153+
context.setMessage("This field is mandatory", TextComponentContext.MessageLevel.ERROR);
154+
break;
155+
}
156+
return true;
157+
default:
158+
break;
159+
}
160+
return false;
161+
}
162+
163+
private Number parseNumber(String input) {
164+
if (!StringUtils.hasText(input)) {
165+
return null;
166+
}
167+
168+
try {
169+
return NumberUtils.parseNumber(input, clazz);
170+
}
171+
catch (NumberFormatException e) {
172+
return null;
173+
}
174+
}
175+
176+
private void checkInput(String input, NumberInputContext context) {
177+
if (!StringUtils.hasText(input)) {
178+
context.setMessage(null);
179+
return;
180+
}
181+
Number num = parseNumber(input);
182+
if (num == null) {
183+
printInvalidInput(input, context);
184+
}
185+
else {
186+
context.setMessage(null);
187+
}
188+
}
189+
190+
private void printInvalidInput(String input, NumberInputContext context) {
191+
String msg = String.format("Sorry, your input is invalid: '%s', try again", input);
192+
context.setMessage(msg, MessageLevel.ERROR);
193+
}
194+
195+
public interface NumberInputContext extends TextComponentContext<Number, NumberInputContext> {
196+
197+
/**
198+
* Gets a default value.
199+
* @return a default value
200+
*/
201+
@Nullable Number getDefaultValue();
202+
203+
/**
204+
* Sets a default value.
205+
* @param defaultValue the default value
206+
*/
207+
void setDefaultValue(@Nullable Number defaultValue);
208+
209+
/**
210+
* Gets a default number class.
211+
* @return a default number class
212+
*/
213+
Class<? extends Number> getDefaultClass();
214+
215+
/**
216+
* Sets a default number class.
217+
* @param defaultClass the default number class
218+
*/
219+
void setDefaultClass(Class<? extends Number> defaultClass);
220+
221+
/**
222+
* Sets flag for mandatory input.
223+
* @param required true if input is required
224+
*/
225+
void setRequired(boolean required);
226+
227+
/**
228+
* Returns flag if input is required.
229+
* @return true if input is required, false otherwise
230+
*/
231+
boolean isRequired();
232+
233+
/**
234+
* Gets an empty {@link NumberInputContext}.
235+
* @return empty number input context
236+
*/
237+
public static NumberInputContext empty() {
238+
return of(null);
239+
}
240+
241+
/**
242+
* Gets an {@link NumberInputContext}.
243+
* @return number input context
244+
*/
245+
public static NumberInputContext of(@Nullable Number defaultValue) {
246+
return new DefaultNumberInputContext(defaultValue, Integer.class, false);
247+
}
248+
249+
/**
250+
* Gets an {@link NumberInputContext}.
251+
* @return number input context
252+
*/
253+
public static NumberInputContext of(@Nullable Number defaultValue, Class<? extends Number> defaultClass) {
254+
return new DefaultNumberInputContext(defaultValue, defaultClass, false);
255+
}
256+
257+
/**
258+
* Gets an {@link NumberInputContext}.
259+
* @return number input context
260+
*/
261+
public static NumberInputContext of(@Nullable Number defaultValue, Class<? extends Number> defaultClass,
262+
boolean required) {
263+
return new DefaultNumberInputContext(defaultValue, defaultClass, required);
264+
}
265+
266+
}
267+
268+
private static class DefaultNumberInputContext extends BaseTextComponentContext<Number, NumberInputContext>
269+
implements NumberInputContext {
270+
271+
private @Nullable Number defaultValue;
272+
273+
private Class<? extends Number> defaultClass;
274+
275+
private boolean required;
276+
277+
public DefaultNumberInputContext(@Nullable Number defaultValue, Class<? extends Number> defaultClass,
278+
boolean required) {
279+
this.defaultValue = defaultValue;
280+
this.defaultClass = defaultClass;
281+
this.required = required;
282+
}
283+
284+
@Override
285+
public @Nullable Number getDefaultValue() {
286+
return defaultValue;
287+
}
288+
289+
@Override
290+
public void setDefaultValue(@Nullable Number defaultValue) {
291+
this.defaultValue = defaultValue;
292+
}
293+
294+
@Override
295+
public Class<? extends Number> getDefaultClass() {
296+
return defaultClass;
297+
}
298+
299+
@Override
300+
public void setDefaultClass(Class<? extends Number> defaultClass) {
301+
this.defaultClass = defaultClass;
302+
}
303+
304+
@Override
305+
public void setRequired(boolean required) {
306+
this.required = required;
307+
}
308+
309+
@Override
310+
public boolean isRequired() {
311+
return required;
312+
}
313+
314+
@Override
315+
public Map<String, @Nullable Object> toTemplateModel() {
316+
Map<String, @Nullable Object> attributes = super.toTemplateModel();
317+
attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null);
318+
attributes.put("defaultClass", getDefaultClass().getSimpleName());
319+
attributes.put("required", isRequired());
320+
Map<String, Object> model = new HashMap<>();
321+
model.put("model", attributes);
322+
return model;
323+
}
324+
325+
}
326+
327+
private class DefaultRenderer implements Function<NumberInputContext, List<AttributedString>> {
328+
329+
@Override
330+
public List<AttributedString> apply(NumberInputContext context) {
331+
return renderTemplateResource(context.toTemplateModel());
332+
}
333+
334+
}
335+
336+
}

0 commit comments

Comments
 (0)