Skip to content

Commit 6a5139f

Browse files
committed
feat: adding number input type
Signed-off-by: Nicola Di Falco <[email protected]>
1 parent f791234 commit 6a5139f

File tree

8 files changed

+1133
-5
lines changed

8 files changed

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

0 commit comments

Comments
 (0)