Skip to content

Commit 61e64bc

Browse files
committed
feat: adding number input type
Signed-off-by: Nicola Di Falco <[email protected]>
1 parent 2f00d73 commit 61e64bc

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+
}

0 commit comments

Comments
 (0)