Skip to content

Commit 255ff77

Browse files
author
Nicola Di Falco
committed
feat: adding number input type
1 parent 0ab3785 commit 255ff77

File tree

10 files changed

+1035
-11
lines changed

10 files changed

+1035
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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 NumberInputContext currentContext;
48+
49+
public NumberInput(Terminal terminal) {
50+
this(terminal, null);
51+
}
52+
53+
public NumberInput(Terminal terminal, String name) {
54+
this(terminal, name, null);
55+
}
56+
57+
public NumberInput(Terminal terminal, String name, Number defaultValue) {
58+
this(terminal, name, defaultValue, Integer.class, null);
59+
}
60+
61+
public NumberInput(Terminal terminal, String name, Number defaultValue, Class<? extends Number> clazz) {
62+
this(terminal, name, defaultValue, clazz, null);
63+
}
64+
65+
public NumberInput(Terminal terminal, String name, Number defaultValue, Class<? extends Number> clazz,
66+
Function<NumberInputContext, List<AttributedString>> renderer) {
67+
super(terminal, name, null);
68+
setRenderer(renderer != null ? renderer : new DefaultRenderer());
69+
setTemplateLocation("classpath:org/springframework/shell/component/number-input-default.stg");
70+
this.defaultValue = defaultValue;
71+
this.clazz = clazz;
72+
}
73+
74+
public void setNumberClass(Class<? extends Number> clazz) {
75+
this.clazz = clazz;
76+
}
77+
78+
@Override
79+
public NumberInputContext getThisContext(ComponentContext<?> context) {
80+
if (context != null && currentContext == context) {
81+
return currentContext;
82+
}
83+
currentContext = NumberInputContext.of(defaultValue, clazz);
84+
currentContext.setName(getName());
85+
Optional.ofNullable(context).map(ComponentContext::stream)
86+
.ifPresent(entryStream -> entryStream.forEach(e -> currentContext.put(e.getKey(), e.getValue())));
87+
return currentContext;
88+
}
89+
90+
@Override
91+
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, NumberInputContext context) {
92+
String operation = bindingReader.readBinding(keyMap);
93+
log.debug("Binding read result {}", operation);
94+
if (operation == null) {
95+
return true;
96+
}
97+
String input;
98+
switch (operation) {
99+
case OPERATION_CHAR:
100+
String lastBinding = bindingReader.getLastBinding();
101+
input = context.getInput();
102+
if (input == null) {
103+
input = lastBinding;
104+
} else {
105+
input = input + lastBinding;
106+
}
107+
context.setInput(input);
108+
checkInput(input, context);
109+
break;
110+
case OPERATION_BACKSPACE:
111+
input = context.getInput();
112+
if (StringUtils.hasLength(input)) {
113+
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
114+
}
115+
context.setInput(input);
116+
checkInput(input, context);
117+
break;
118+
case OPERATION_EXIT:
119+
if (StringUtils.hasText(context.getInput())) {
120+
context.setResultValue(parseNumber(context.getInput()));
121+
}
122+
else if (context.getDefaultValue() != null) {
123+
context.setResultValue(context.getDefaultValue());
124+
}
125+
return true;
126+
default:
127+
break;
128+
}
129+
return false;
130+
}
131+
132+
private Number parseNumber(String input) {
133+
if (!StringUtils.hasText(input)) {
134+
return null;
135+
}
136+
137+
try {
138+
return NumberUtils.parseNumber(input, clazz);
139+
} catch (NumberFormatException e) {
140+
return null;
141+
}
142+
}
143+
144+
private void checkInput(String input, NumberInputContext context) {
145+
if (!StringUtils.hasText(input)) {
146+
context.setMessage(null);
147+
return;
148+
}
149+
Number num = parseNumber(input);
150+
if (num == null) {
151+
String msg = String.format("Sorry, your input is invalid: '%s', try again", input);
152+
context.setMessage(msg, MessageLevel.ERROR);
153+
}
154+
else {
155+
context.setMessage(null);
156+
}
157+
}
158+
159+
public interface NumberInputContext extends TextComponentContext<Number, NumberInputContext> {
160+
161+
/**
162+
* Gets a default value.
163+
*
164+
* @return a default value
165+
*/
166+
Number getDefaultValue();
167+
168+
/**
169+
* Sets a default value.
170+
*
171+
* @param defaultValue the default value
172+
*/
173+
void setDefaultValue(Number defaultValue);
174+
175+
/**
176+
* Gets a default number class.
177+
*
178+
* @return a default number class
179+
*/
180+
Class<? extends Number> getDefaultClass();
181+
182+
/**
183+
* Sets a default number class.
184+
*
185+
* @param defaultClass the default number class
186+
*/
187+
void setDefaultClass(Class<? extends Number> defaultClass);
188+
189+
/**
190+
* Gets an empty {@link NumberInputContext}.
191+
*
192+
* @return empty number input context
193+
*/
194+
public static NumberInputContext empty() {
195+
return of(null);
196+
}
197+
198+
/**
199+
* Gets an {@link NumberInputContext}.
200+
*
201+
* @return number input context
202+
*/
203+
public static NumberInputContext of(Number defaultValue) {
204+
return new DefaultNumberInputContext(defaultValue, Integer.class);
205+
}
206+
207+
/**
208+
* Gets an {@link NumberInputContext}.
209+
*
210+
* @return number input context
211+
*/
212+
public static NumberInputContext of(Number defaultValue, Class<? extends Number> defaultClass) {
213+
return new DefaultNumberInputContext(defaultValue, defaultClass);
214+
}
215+
}
216+
217+
private static class DefaultNumberInputContext extends BaseTextComponentContext<Number, NumberInputContext> implements NumberInputContext {
218+
219+
private Number defaultValue;
220+
private Class<? extends Number> defaultClass;
221+
222+
public DefaultNumberInputContext(Number defaultValue, Class<? extends Number> defaultClass) {
223+
this.defaultValue = defaultValue;
224+
this.defaultClass = defaultClass;
225+
}
226+
227+
@Override
228+
public Number getDefaultValue() {
229+
return defaultValue;
230+
}
231+
232+
@Override
233+
public void setDefaultValue(Number defaultValue) {
234+
this.defaultValue = defaultValue;
235+
}
236+
237+
@Override
238+
public Class<? extends Number> getDefaultClass() {
239+
return defaultClass;
240+
}
241+
242+
@Override
243+
public void setDefaultClass(Class<? extends Number> defaultClass) {
244+
this.defaultClass = defaultClass;
245+
}
246+
247+
@Override
248+
public Map<String, Object> toTemplateModel() {
249+
Map<String, Object> attributes = super.toTemplateModel();
250+
attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null);
251+
attributes.put("defaultClass", getDefaultClass().getSimpleName());
252+
Map<String, Object> model = new HashMap<>();
253+
model.put("model", attributes);
254+
return model;
255+
}
256+
}
257+
258+
private class DefaultRenderer implements Function<NumberInputContext, List<AttributedString>> {
259+
260+
@Override
261+
public List<AttributedString> apply(NumberInputContext context) {
262+
return renderTemplateResource(context.toTemplateModel());
263+
}
264+
}
265+
}

0 commit comments

Comments
 (0)