Skip to content

Commit ea917b4

Browse files
committed
Add confirmation component
- New ConfirmationInput component - Add missing native resource config for templates. - Add sample "component confirmation", use flag --no to switch default from yes to no. - Fixes spring-projects#366
1 parent 2f42097 commit ea917b4

File tree

5 files changed

+435
-0
lines changed

5 files changed

+435
-0
lines changed
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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.function.Function;
22+
23+
import org.jline.keymap.BindingReader;
24+
import org.jline.keymap.KeyMap;
25+
import org.jline.terminal.Terminal;
26+
import org.jline.utils.AttributedString;
27+
28+
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
29+
import org.springframework.shell.component.context.ComponentContext;
30+
import org.springframework.shell.component.support.AbstractTextComponent;
31+
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext;
32+
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext.MessageLevel;
33+
import org.springframework.util.StringUtils;
34+
35+
/**
36+
* Component for a confirmation question.
37+
*
38+
* @author Janne Valkealahti
39+
*/
40+
public class ConfirmationInput extends AbstractTextComponent<Boolean, ConfirmationInputContext> {
41+
42+
private final boolean defaultValue;
43+
private ConfirmationInputContext currentContext;
44+
45+
public ConfirmationInput(Terminal terminal) {
46+
this(terminal, null);
47+
}
48+
49+
public ConfirmationInput(Terminal terminal, String name) {
50+
this(terminal, name, true, null);
51+
}
52+
53+
public ConfirmationInput(Terminal terminal, String name, boolean defaultValue) {
54+
this(terminal, name, defaultValue, null);
55+
}
56+
57+
public ConfirmationInput(Terminal terminal, String name, boolean defaultValue,
58+
Function<ConfirmationInputContext, List<AttributedString>> renderer) {
59+
super(terminal, name, null);
60+
setRenderer(renderer != null ? renderer : new DefaultRenderer());
61+
setTemplateLocation("classpath:org/springframework/shell/component/confirmation-input-default.stg");
62+
this.defaultValue = defaultValue;
63+
}
64+
65+
@Override
66+
protected ConfirmationInputContext getThisContext(ComponentContext<?> context) {
67+
if (context != null && currentContext == context) {
68+
return currentContext;
69+
}
70+
currentContext = ConfirmationInputContext.of(defaultValue);
71+
currentContext.setName(getName());
72+
context.stream().forEach(e -> {
73+
currentContext.put(e.getKey(), e.getValue());
74+
});
75+
return currentContext;
76+
}
77+
78+
@Override
79+
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, ConfirmationInputContext context) {
80+
String operation = bindingReader.readBinding(keyMap);
81+
String input;
82+
switch (operation) {
83+
case OPERATION_CHAR:
84+
String lastBinding = bindingReader.getLastBinding();
85+
input = context.getInput();
86+
if (input == null) {
87+
input = lastBinding;
88+
}
89+
else {
90+
input = input + lastBinding;
91+
}
92+
context.setInput(input);
93+
checkInput(input, context);
94+
break;
95+
case OPERATION_BACKSPACE:
96+
input = context.getInput();
97+
if (StringUtils.hasLength(input)) {
98+
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
99+
}
100+
context.setInput(input);
101+
checkInput(input, context);
102+
break;
103+
case OPERATION_EXIT:
104+
if (StringUtils.hasText(context.getInput())) {
105+
context.setResultValue(parseBoolean(context.getInput()));
106+
}
107+
else if (context.getDefaultValue() != null) {
108+
context.setResultValue(context.getDefaultValue());
109+
}
110+
return true;
111+
default:
112+
break;
113+
}
114+
return false;
115+
}
116+
117+
private Boolean parseBoolean(String input) {
118+
if (!StringUtils.hasText(input)) {
119+
return null;
120+
}
121+
input = input.trim().toLowerCase();
122+
switch (input) {
123+
case "y":
124+
case "yes":
125+
case "1":
126+
return true;
127+
case "n":
128+
case "no":
129+
case "0":
130+
return false;
131+
default:
132+
return null;
133+
}
134+
}
135+
136+
private void checkInput(String input, ConfirmationInputContext context) {
137+
if (!StringUtils.hasText(input)) {
138+
context.setMessage(null);
139+
return;
140+
}
141+
Boolean yesno = parseBoolean(input);
142+
if (yesno == null) {
143+
String msg = String.format("Sorry, your input is invalid: '%s', try again", input);
144+
context.setMessage(msg, MessageLevel.ERROR);
145+
}
146+
else {
147+
context.setMessage(null);
148+
}
149+
}
150+
151+
public interface ConfirmationInputContext extends TextComponentContext<Boolean, ConfirmationInputContext> {
152+
153+
/**
154+
* Gets a default value.
155+
*
156+
* @return a default value
157+
*/
158+
Boolean getDefaultValue();
159+
160+
/**
161+
* Sets a default value.
162+
*
163+
* @param defaultValue the default value
164+
*/
165+
void setDefaultValue(Boolean defaultValue);
166+
167+
/**
168+
* Gets an empty {@link ConfirmationInputContext}.
169+
*
170+
* @return empty path input context
171+
*/
172+
public static ConfirmationInputContext empty() {
173+
return of(null);
174+
}
175+
176+
/**
177+
* Gets an {@link ConfirmationInputContext}.
178+
*
179+
* @return path input context
180+
*/
181+
public static ConfirmationInputContext of(Boolean defaultValue) {
182+
return new DefaultConfirmationInputContext(defaultValue);
183+
}
184+
}
185+
186+
private static class DefaultConfirmationInputContext extends BaseTextComponentContext<Boolean, ConfirmationInputContext>
187+
implements ConfirmationInputContext {
188+
189+
private Boolean defaultValue;
190+
191+
public DefaultConfirmationInputContext(Boolean defaultValue) {
192+
this.defaultValue = defaultValue;
193+
}
194+
195+
@Override
196+
public Boolean getDefaultValue() {
197+
return defaultValue;
198+
}
199+
200+
@Override
201+
public void setDefaultValue(Boolean defaultValue) {
202+
this.defaultValue = defaultValue;
203+
}
204+
205+
@Override
206+
public Map<String, Object> toTemplateModel() {
207+
Map<String, Object> attributes = super.toTemplateModel();
208+
attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null);
209+
Map<String, Object> model = new HashMap<>();
210+
model.put("model", attributes);
211+
return model;
212+
}
213+
}
214+
215+
private class DefaultRenderer implements Function<ConfirmationInputContext, List<AttributedString>> {
216+
217+
@Override
218+
public List<AttributedString> apply(ConfirmationInputContext context) {
219+
return renderTemplateResource(context.toTemplateModel());
220+
}
221+
}
222+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"resources": {
3+
"includes": [
4+
{
5+
"pattern": "org/springframework/shell/component/.*.stg"
6+
}
7+
]
8+
}
9+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// message
2+
message(model) ::= <%
3+
<if(model.message && model.hasMessageLevelError)>
4+
<(">>>"); format="level-error"> <model.message; format="level-error">
5+
<elseif(model.message && model.hasMessageLevelWarn)>
6+
<(">>"); format="level-warn"> <model.message; format="level-warn">
7+
<elseif(model.message && model.hasMessageLevelInfo)>
8+
<(">"); format="level-info"> <model.message; format="level-info">
9+
<endif>
10+
%>
11+
12+
// info section after '? xxx'
13+
info(model) ::= <%
14+
<if(model.defaultValue)>
15+
<("(Y/n)"); format="item-disabled">
16+
<else>
17+
<("(y/N)"); format="item-disabled">
18+
<endif>
19+
%>
20+
21+
// start '? xxx' shows both running and result
22+
question_name(model) ::= <<
23+
<("?"); format="list-value"> <model.name; format="title">
24+
>>
25+
26+
// component result
27+
result(model) ::= <<
28+
<question_name(model)> <model.resultValue; format="value">
29+
>>
30+
31+
// component is running
32+
running(model) ::= <<
33+
<question_name(model)> <info(model)>
34+
<message(model)>
35+
>>
36+
37+
// main
38+
main(model) ::= <<
39+
<if(model.resultValue)><result(model)><else><running(model)><endif>
40+
>>

0 commit comments

Comments
 (0)