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 #366
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
|
||||
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext.MessageLevel;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Component for a confirmation question.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ConfirmationInput extends AbstractTextComponent<Boolean, ConfirmationInputContext> {
|
||||
|
||||
private final boolean defaultValue;
|
||||
private ConfirmationInputContext currentContext;
|
||||
|
||||
public ConfirmationInput(Terminal terminal) {
|
||||
this(terminal, null);
|
||||
}
|
||||
|
||||
public ConfirmationInput(Terminal terminal, String name) {
|
||||
this(terminal, name, true, null);
|
||||
}
|
||||
|
||||
public ConfirmationInput(Terminal terminal, String name, boolean defaultValue) {
|
||||
this(terminal, name, defaultValue, null);
|
||||
}
|
||||
|
||||
public ConfirmationInput(Terminal terminal, String name, boolean defaultValue,
|
||||
Function<ConfirmationInputContext, List<AttributedString>> renderer) {
|
||||
super(terminal, name, null);
|
||||
setRenderer(renderer != null ? renderer : new DefaultRenderer());
|
||||
setTemplateLocation("classpath:org/springframework/shell/component/confirmation-input-default.stg");
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfirmationInputContext getThisContext(ComponentContext<?> context) {
|
||||
if (context != null && currentContext == context) {
|
||||
return currentContext;
|
||||
}
|
||||
currentContext = ConfirmationInputContext.of(defaultValue);
|
||||
currentContext.setName(getName());
|
||||
context.stream().forEach(e -> {
|
||||
currentContext.put(e.getKey(), e.getValue());
|
||||
});
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, ConfirmationInputContext context) {
|
||||
String operation = bindingReader.readBinding(keyMap);
|
||||
String input;
|
||||
switch (operation) {
|
||||
case OPERATION_CHAR:
|
||||
String lastBinding = bindingReader.getLastBinding();
|
||||
input = context.getInput();
|
||||
if (input == null) {
|
||||
input = lastBinding;
|
||||
}
|
||||
else {
|
||||
input = input + lastBinding;
|
||||
}
|
||||
context.setInput(input);
|
||||
checkInput(input, context);
|
||||
break;
|
||||
case OPERATION_BACKSPACE:
|
||||
input = context.getInput();
|
||||
if (StringUtils.hasLength(input)) {
|
||||
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
|
||||
}
|
||||
context.setInput(input);
|
||||
checkInput(input, context);
|
||||
break;
|
||||
case OPERATION_EXIT:
|
||||
if (StringUtils.hasText(context.getInput())) {
|
||||
context.setResultValue(parseBoolean(context.getInput()));
|
||||
}
|
||||
else if (context.getDefaultValue() != null) {
|
||||
context.setResultValue(context.getDefaultValue());
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Boolean parseBoolean(String input) {
|
||||
if (!StringUtils.hasText(input)) {
|
||||
return null;
|
||||
}
|
||||
input = input.trim().toLowerCase();
|
||||
switch (input) {
|
||||
case "y":
|
||||
case "yes":
|
||||
case "1":
|
||||
return true;
|
||||
case "n":
|
||||
case "no":
|
||||
case "0":
|
||||
return false;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkInput(String input, ConfirmationInputContext context) {
|
||||
if (!StringUtils.hasText(input)) {
|
||||
context.setMessage(null);
|
||||
return;
|
||||
}
|
||||
Boolean yesno = parseBoolean(input);
|
||||
if (yesno == null) {
|
||||
String msg = String.format("Sorry, your input is invalid: '%s', try again", input);
|
||||
context.setMessage(msg, MessageLevel.ERROR);
|
||||
}
|
||||
else {
|
||||
context.setMessage(null);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ConfirmationInputContext extends TextComponentContext<Boolean, ConfirmationInputContext> {
|
||||
|
||||
/**
|
||||
* Gets a default value.
|
||||
*
|
||||
* @return a default value
|
||||
*/
|
||||
Boolean getDefaultValue();
|
||||
|
||||
/**
|
||||
* Sets a default value.
|
||||
*
|
||||
* @param defaultValue the default value
|
||||
*/
|
||||
void setDefaultValue(Boolean defaultValue);
|
||||
|
||||
/**
|
||||
* Gets an empty {@link ConfirmationInputContext}.
|
||||
*
|
||||
* @return empty path input context
|
||||
*/
|
||||
public static ConfirmationInputContext empty() {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an {@link ConfirmationInputContext}.
|
||||
*
|
||||
* @return path input context
|
||||
*/
|
||||
public static ConfirmationInputContext of(Boolean defaultValue) {
|
||||
return new DefaultConfirmationInputContext(defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultConfirmationInputContext extends BaseTextComponentContext<Boolean, ConfirmationInputContext>
|
||||
implements ConfirmationInputContext {
|
||||
|
||||
private Boolean defaultValue;
|
||||
|
||||
public DefaultConfirmationInputContext(Boolean defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultValue(Boolean defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = super.toTemplateModel();
|
||||
attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null);
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("model", attributes);
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultRenderer implements Function<ConfirmationInputContext, List<AttributedString>> {
|
||||
|
||||
@Override
|
||||
public List<AttributedString> apply(ConfirmationInputContext context) {
|
||||
return renderTemplateResource(context.toTemplateModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"resources": {
|
||||
"includes": [
|
||||
{
|
||||
"pattern": "org/springframework/shell/component/.*.stg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// message
|
||||
message(model) ::= <%
|
||||
<if(model.message && model.hasMessageLevelError)>
|
||||
<(">>>"); format="level-error"> <model.message; format="level-error">
|
||||
<elseif(model.message && model.hasMessageLevelWarn)>
|
||||
<(">>"); format="level-warn"> <model.message; format="level-warn">
|
||||
<elseif(model.message && model.hasMessageLevelInfo)>
|
||||
<(">"); format="level-info"> <model.message; format="level-info">
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// info section after '? xxx'
|
||||
info(model) ::= <%
|
||||
<if(model.defaultValue)>
|
||||
<("(Y/n)"); format="item-disabled">
|
||||
<else>
|
||||
<("(y/N)"); format="item-disabled">
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// start '? xxx' shows both running and result
|
||||
question_name(model) ::= <<
|
||||
<("?"); format="list-value"> <model.name; format="title">
|
||||
>>
|
||||
|
||||
// component result
|
||||
result(model) ::= <<
|
||||
<question_name(model)> <model.resultValue; format="value">
|
||||
>>
|
||||
|
||||
// component is running
|
||||
running(model) ::= <<
|
||||
<question_name(model)> <info(model)>
|
||||
<message(model)>
|
||||
>>
|
||||
|
||||
// main
|
||||
main(model) ::= <<
|
||||
<if(model.resultValue)><result(model)><else><running(model)><endif>
|
||||
>>
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ConfirmationInputTests extends AbstractShellTests {
|
||||
|
||||
private ExecutorService service;
|
||||
private CountDownLatch latch1;
|
||||
private AtomicReference<ConfirmationInputContext> result1;
|
||||
|
||||
@BeforeEach
|
||||
public void setupTests() {
|
||||
service = Executors.newFixedThreadPool(1);
|
||||
latch1 = new CountDownLatch(1);
|
||||
result1 = new AtomicReference<>();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanupTests() throws IOException {
|
||||
latch1 = null;
|
||||
result1 = null;
|
||||
if (service != null) {
|
||||
service.shutdown();
|
||||
}
|
||||
service = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultUserInputEnterDefaultYes() throws InterruptedException, IOException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
ConfirmationInput component1 = new ConfirmationInput(getTerminal(), "component1");
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
ConfirmationInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
ConfirmationInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultUserInputEnterDefaultNo() throws InterruptedException, IOException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
ConfirmationInput component1 = new ConfirmationInput(getTerminal(), "component1", false);
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
ConfirmationInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
ConfirmationInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultUserInputNo() throws InterruptedException, IOException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
ConfirmationInput component1 = new ConfirmationInput(getTerminal(), "component1");
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
ConfirmationInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().append("no").cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
ConfirmationInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultUserInputYes() throws InterruptedException, IOException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
ConfirmationInput component1 = new ConfirmationInput(getTerminal(), "component1");
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
ConfirmationInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().append("yes").cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
ConfirmationInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,12 @@ import java.util.stream.Collectors;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.component.ConfirmationInput;
|
||||
import org.springframework.shell.component.MultiItemSelector;
|
||||
import org.springframework.shell.component.PathInput;
|
||||
import org.springframework.shell.component.SingleItemSelector;
|
||||
import org.springframework.shell.component.StringInput;
|
||||
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
|
||||
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
|
||||
import org.springframework.shell.component.PathInput.PathInputContext;
|
||||
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
|
||||
@@ -71,6 +73,15 @@ public class ComponentCommands extends AbstractShellComponent implements Resourc
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
|
||||
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
|
||||
public String confirmationInput(boolean no) {
|
||||
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);
|
||||
component.setResourceLoader(resourceLoader);
|
||||
component.setTemplateExecutor(templateExecutor);
|
||||
ConfirmationInputContext context = component.run(ConfirmationInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
|
||||
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
|
||||
public String singleSelector() {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
|
||||
Reference in New Issue
Block a user