diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/NumberInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/NumberInput.java new file mode 100644 index 000000000..c748a7797 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/NumberInput.java @@ -0,0 +1,323 @@ +/* + * 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.Optional; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.shell.component.NumberInput.NumberInputContext; +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.NumberUtils; +import org.springframework.util.StringUtils; + +/** + * Component for a number input. + * + * @author Nicola Di Falco + */ +public class NumberInput extends AbstractTextComponent { + + private static final Logger log = LoggerFactory.getLogger(NumberInput.class); + private final Number defaultValue; + private Class clazz; + private boolean required; + private NumberInputContext currentContext; + + public NumberInput(Terminal terminal) { + this(terminal, null); + } + + public NumberInput(Terminal terminal, String name) { + this(terminal, name, null); + } + + public NumberInput(Terminal terminal, String name, Number defaultValue) { + this(terminal, name, defaultValue, Integer.class); + } + + public NumberInput(Terminal terminal, String name, Number defaultValue, Class clazz) { + this(terminal, name, defaultValue, clazz, false); + } + + public NumberInput(Terminal terminal, String name, Number defaultValue, Class clazz, boolean required) { + this(terminal, name, defaultValue, clazz, required, null); + } + + public NumberInput(Terminal terminal, String name, Number defaultValue, Class clazz, boolean required, + Function> renderer) { + super(terminal, name, null); + setRenderer(renderer != null ? renderer : new DefaultRenderer()); + setTemplateLocation("classpath:org/springframework/shell/component/number-input-default.stg"); + this.defaultValue = defaultValue; + this.clazz = clazz; + this.required = required; + } + + public void setNumberClass(Class clazz) { + this.clazz = clazz; + } + + public void setRequired(boolean required) { + this.required = required; + } + + @Override + public NumberInputContext getThisContext(ComponentContext context) { + if (context != null && currentContext == context) { + return currentContext; + } + currentContext = NumberInputContext.of(defaultValue, clazz, required); + currentContext.setName(getName()); + Optional.ofNullable(context).map(ComponentContext::stream) + .ifPresent(entryStream -> entryStream.forEach(e -> currentContext.put(e.getKey(), e.getValue()))); + return currentContext; + } + + @Override + protected boolean read(BindingReader bindingReader, KeyMap keyMap, NumberInputContext context) { + String operation = bindingReader.readBinding(keyMap); + log.debug("Binding read result {}", operation); + if (operation == null) { + return true; + } + 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: + Number num = parseNumber(context.getInput()); + + if (num != null) { + context.setResultValue(parseNumber(context.getInput())); + } else if (StringUtils.hasText(context.getInput())) { + printInvalidInput(context.getInput(), context); + break; + } else if (context.getDefaultValue() != null) { + context.setResultValue(context.getDefaultValue()); + } else if (required) { + context.setMessage("This field is mandatory", TextComponentContext.MessageLevel.ERROR); + break; + } + return true; + default: + break; + } + return false; + } + + private Number parseNumber(String input) { + if (!StringUtils.hasText(input)) { + return null; + } + + try { + return NumberUtils.parseNumber(input, clazz); + } catch (NumberFormatException e) { + return null; + } + } + + private void checkInput(String input, NumberInputContext context) { + if (!StringUtils.hasText(input)) { + context.setMessage(null); + return; + } + Number num = parseNumber(input); + if (num == null) { + printInvalidInput(input, context); + } + else { + context.setMessage(null); + } + } + + private void printInvalidInput(String input, NumberInputContext context) { + String msg = String.format("Sorry, your input is invalid: '%s', try again", input); + context.setMessage(msg, MessageLevel.ERROR); + } + + public interface NumberInputContext extends TextComponentContext { + + /** + * Gets a default value. + * + * @return a default value + */ + Number getDefaultValue(); + + /** + * Sets a default value. + * + * @param defaultValue the default value + */ + void setDefaultValue(Number defaultValue); + + /** + * Gets a default number class. + * + * @return a default number class + */ + Class getDefaultClass(); + + /** + * Sets a default number class. + * + * @param defaultClass the default number class + */ + void setDefaultClass(Class defaultClass); + + /** + * Sets flag for mandatory input. + * + * @param required true if input is required + */ + void setRequired(boolean required); + + /** + * Returns flag if input is required. + * + * @return true if input is required, false otherwise + */ + boolean isRequired(); + + /** + * Gets an empty {@link NumberInputContext}. + * + * @return empty number input context + */ + public static NumberInputContext empty() { + return of(null); + } + + /** + * Gets an {@link NumberInputContext}. + * + * @return number input context + */ + public static NumberInputContext of(Number defaultValue) { + return new DefaultNumberInputContext(defaultValue, Integer.class, false); + } + + /** + * Gets an {@link NumberInputContext}. + * + * @return number input context + */ + public static NumberInputContext of(Number defaultValue, Class defaultClass) { + return new DefaultNumberInputContext(defaultValue, defaultClass, false); + } + + /** + * Gets an {@link NumberInputContext}. + * + * @return number input context + */ + public static NumberInputContext of(Number defaultValue, Class defaultClass, boolean required) { + return new DefaultNumberInputContext(defaultValue, defaultClass, required); + } + } + + private static class DefaultNumberInputContext extends BaseTextComponentContext implements NumberInputContext { + + private Number defaultValue; + private Class defaultClass; + private boolean required; + + public DefaultNumberInputContext(Number defaultValue, Class defaultClass, boolean required) { + this.defaultValue = defaultValue; + this.defaultClass = defaultClass; + this.required = required; + } + + @Override + public Number getDefaultValue() { + return defaultValue; + } + + @Override + public void setDefaultValue(Number defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public Class getDefaultClass() { + return defaultClass; + } + + @Override + public void setDefaultClass(Class defaultClass) { + this.defaultClass = defaultClass; + } + + @Override + public void setRequired(boolean required) { + this.required = required; + } + + @Override + public boolean isRequired() { + return required; + } + + @Override + public Map toTemplateModel() { + Map attributes = super.toTemplateModel(); + attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null); + attributes.put("defaultClass", getDefaultClass().getSimpleName()); + attributes.put("required", isRequired()); + Map model = new HashMap<>(); + model.put("model", attributes); + return model; + } + } + + private class DefaultRenderer implements Function> { + + @Override + public List apply(NumberInputContext context) { + return renderTemplateResource(context.toTemplateModel()); + } + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseNumberInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseNumberInput.java new file mode 100644 index 000000000..d0021670c --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseNumberInput.java @@ -0,0 +1,182 @@ +/* + * 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.flow; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.jline.utils.AttributedString; +import org.springframework.shell.component.NumberInput.NumberInputContext; +import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder; +import org.springframework.shell.component.flow.ComponentFlow.Builder; + +/** + * Base impl for {@link NumberInputSpec}. + * + * @author Nicola Di Falco + */ +public abstract class BaseNumberInput extends BaseInput implements NumberInputSpec { + + private String name; + private Number resultValue; + private ResultMode resultMode; + private Number defaultValue; + private Class clazz = Integer.class; + private boolean required = false; + private Function> renderer; + private final List> preHandlers = new ArrayList<>(); + private final List> postHandlers = new ArrayList<>(); + private boolean storeResult = true; + private String templateLocation; + private Function next; + + protected BaseNumberInput(BaseBuilder builder, String id) { + super(builder, id); + } + + @Override + public NumberInputSpec name(String name) { + this.name = name; + return this; + } + + @Override + public NumberInputSpec resultValue(Number resultValue) { + this.resultValue = resultValue; + return this; + } + + @Override + public NumberInputSpec resultMode(ResultMode resultMode) { + this.resultMode = resultMode; + return this; + } + + @Override + public NumberInputSpec defaultValue(Number defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + @Override + public NumberInputSpec numberClass(Class clazz) { + this.clazz = clazz; + return this; + } + + @Override + public NumberInputSpec required() { + this.required = true; + return this; + } + + @Override + public NumberInputSpec renderer(Function> renderer) { + this.renderer = renderer; + return this; + } + + @Override + public NumberInputSpec template(String location) { + this.templateLocation = location; + return this; + } + + @Override + public NumberInputSpec preHandler(Consumer handler) { + this.preHandlers.add(handler); + return this; + } + + @Override + public NumberInputSpec postHandler(Consumer handler) { + this.postHandlers.add(handler); + return this; + } + + @Override + public NumberInputSpec storeResult(boolean store) { + this.storeResult = store; + return this; + } + + @Override + public NumberInputSpec next(Function next) { + this.next = next; + return this; + } + + @Override + public Builder and() { + getBuilder().addNumberInput(this); + return getBuilder(); + } + + @Override + public NumberInputSpec getThis() { + return this; + } + + public String getName() { + return name; + } + + public Number getResultValue() { + return resultValue; + } + + public ResultMode getResultMode() { + return resultMode; + } + + public Number getDefaultValue() { + return defaultValue; + } + + public Class getNumberClass() { + return clazz; + } + + public boolean isRequired() { + return required; + } + + public Function> getRenderer() { + return renderer; + } + + public String getTemplateLocation() { + return templateLocation; + } + + public List> getPreHandlers() { + return preHandlers; + } + + public List> getPostHandlers() { + return postHandlers; + } + + public boolean isStoreResult() { + return storeResult; + } + + public Function getNext() { + return next; + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java index d1ee2c83f..8692ff0f6 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java @@ -25,25 +25,27 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jline.terminal.Terminal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.core.io.ResourceLoader; import org.springframework.shell.component.ConfirmationInput; +import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext; import org.springframework.shell.component.MultiItemSelector; import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext; +import org.springframework.shell.component.NumberInput; +import org.springframework.shell.component.NumberInput.NumberInputContext; import org.springframework.shell.component.PathInput; import org.springframework.shell.component.PathInput.PathInputContext; import org.springframework.shell.component.SingleItemSelector; import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext; import org.springframework.shell.component.StringInput; -import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext; import org.springframework.shell.component.StringInput.StringInputContext; import org.springframework.shell.component.context.ComponentContext; import org.springframework.shell.component.support.SelectorItem; @@ -102,6 +104,14 @@ interface Builder { */ StringInputSpec withStringInput(String id); + /** + * Gets a builder for number input. + * + * @param id the identifier + * @return builder for number input + */ + NumberInputSpec withNumberInput(String id); + /** * Gets a builder for path input. * @@ -183,6 +193,7 @@ interface Builder { static abstract class BaseBuilder implements Builder { private final List stringInputs = new ArrayList<>(); + private final List numberInputs = new ArrayList<>(); private final List pathInputs = new ArrayList<>(); private final List confirmationInputs = new ArrayList<>(); private final List singleItemSelectors = new ArrayList<>(); @@ -198,7 +209,7 @@ static abstract class BaseBuilder implements Builder { @Override public ComponentFlow build() { - return new DefaultComponentFlow(terminal, resourceLoader, templateExecutor, stringInputs, pathInputs, + return new DefaultComponentFlow(terminal, resourceLoader, templateExecutor, stringInputs, numberInputs, pathInputs, confirmationInputs, singleItemSelectors, multiItemSelectors); } @@ -207,6 +218,11 @@ public StringInputSpec withStringInput(String id) { return new DefaultStringInputSpec(this, id); } + @Override + public NumberInputSpec withNumberInput(String id) { + return new DefaultNumberInputSpec(this, id); + } + @Override public PathInputSpec withPathInput(String id) { return new DefaultPathInputSpec(this, id); @@ -253,6 +269,7 @@ public Builder clone() { @Override public Builder reset() { stringInputs.clear(); + numberInputs.clear(); pathInputs.clear(); confirmationInputs.clear(); singleItemSelectors.clear(); @@ -268,6 +285,12 @@ void addStringInput(BaseStringInput input) { stringInputs.add(input); } + void addNumberInput(BaseNumberInput input) { + checkUniqueId(input.getId()); + input.setOrder(order.getAndIncrement()); + numberInputs.add(input); + } + void addPathInput(BasePathInput input) { checkUniqueId(input.getId()); input.setOrder(order.getAndIncrement()); @@ -343,6 +366,7 @@ static class DefaultComponentFlow implements ComponentFlow { private static final Logger log = LoggerFactory.getLogger(DefaultComponentFlow.class); private final Terminal terminal; private final List stringInputs; + private final List numberInputs; private final List pathInputs; private final List confirmationInputs; private final List singleInputs; @@ -351,12 +375,14 @@ static class DefaultComponentFlow implements ComponentFlow { private final TemplateExecutor templateExecutor; DefaultComponentFlow(Terminal terminal, ResourceLoader resourceLoader, TemplateExecutor templateExecutor, - List stringInputs, List pathInputs, List confirmationInputs, - List singleInputs, List multiInputs) { + List stringInputs, List numberInputs, List pathInputs, + List confirmationInputs, List singleInputs, + List multiInputs) { this.terminal = terminal; this.resourceLoader = resourceLoader; this.templateExecutor = templateExecutor; this.stringInputs = stringInputs; + this.numberInputs = numberInputs; this.pathInputs = pathInputs; this.confirmationInputs = confirmationInputs; this.singleInputs = singleInputs; @@ -407,7 +433,7 @@ static class Node { private DefaultComponentFlowResult runGetResults() { List oios = Stream - .of(stringInputsStream(), pathInputsStream(), confirmationInputsStream(), + .of(stringInputsStream(), numberInputsStream(), pathInputsStream(), confirmationInputsStream(), singleItemSelectorsStream(), multiItemSelectorsStream()) .flatMap(oio -> oio) .sorted(OrderComparator.INSTANCE) @@ -487,6 +513,49 @@ private Stream stringInputsStream() { }); } + private Stream numberInputsStream() { + return numberInputs.stream().map(input -> { + NumberInput selector = new NumberInput(terminal, input.getName(), input.getDefaultValue(), input.getNumberClass(), input.isRequired()); + UnaryOperator> operation = context -> { + if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult() + && input.getResultValue() != null) { + context.put(input.getId(), input.getResultValue()); + return context; + } + selector.setResourceLoader(resourceLoader); + selector.setTemplateExecutor(templateExecutor); + selector.setNumberClass(input.getNumberClass()); + if (StringUtils.hasText(input.getTemplateLocation())) { + selector.setTemplateLocation(input.getTemplateLocation()); + } + if (input.getRenderer() != null) { + selector.setRenderer(input.getRenderer()); + } + if (input.isStoreResult()) { + if (input.getResultMode() == ResultMode.VERIFY && input.getResultValue() != null) { + selector.addPreRunHandler(c -> { + c.setDefaultValue(input.getResultValue()); + c.setRequired(input.isRequired()); + }); + } + selector.addPostRunHandler(c -> c.put(input.getId(), c.getResultValue())); + } + for (Consumer handler : input.getPreHandlers()) { + selector.addPreRunHandler(handler); + } + for (Consumer handler : input.getPostHandlers()) { + selector.addPostRunHandler(handler); + } + return selector.run(context); + }; + Function f1 = input.getNext(); + Function, Optional> f2 = context -> f1 != null + ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) + : null; + return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); + }); + } + private Stream pathInputsStream() { return pathInputs.stream().map(input -> { PathInput selector = new PathInput(terminal, input.getName()); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultNumberInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultNumberInputSpec.java new file mode 100644 index 000000000..66f005d1a --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultNumberInputSpec.java @@ -0,0 +1,30 @@ +/* + * 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.flow; + +import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder; + +/** + * Default impl for {@link BaseNumberInput}. + * + * @author Nicola Di Falco + */ +public class DefaultNumberInputSpec extends BaseNumberInput { + + public DefaultNumberInputSpec(BaseBuilder builder, String id) { + super(builder, id); + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/NumberInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/NumberInputSpec.java new file mode 100644 index 000000000..881206a44 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/NumberInputSpec.java @@ -0,0 +1,137 @@ +/* + * 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.flow; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.jline.utils.AttributedString; +import org.springframework.shell.component.NumberInput.NumberInputContext; +import org.springframework.shell.component.context.ComponentContext; +import org.springframework.shell.component.flow.ComponentFlow.Builder; + +/** + * Interface for number input spec builder. + * + * @author Nicola Di Falco + */ +public interface NumberInputSpec extends BaseInputSpec { + + /** + * Sets a name. + * + * @param name the name + * @return a builder + */ + NumberInputSpec name(String name); + + /** + * Sets a result value. + * + * @param resultValue the result value + * @return a builder + */ + NumberInputSpec resultValue(Number resultValue); + + /** + * Sets a result mode. + * + * @param resultMode the result mode + * @return a builder + */ + NumberInputSpec resultMode(ResultMode resultMode); + + /** + * Sets a default value. + * + * @param defaultValue the defult value + * @return a builder + */ + NumberInputSpec defaultValue(Number defaultValue); + + /** + * Sets the class of the number. Defaults to Integer. + * + * @param clazz the specific number class + * @return a builder + */ + NumberInputSpec numberClass(Class clazz); + + /** + * Sets input to required + * + * @return a builder + */ + NumberInputSpec required(); + + /** + * Sets a renderer function. + * + * @param renderer the renderer + * @return a builder + */ + NumberInputSpec renderer(Function> renderer); + + /** + * Sets a default renderer template location. + * + * @param location the template location + * @return a builder + */ + NumberInputSpec template(String location); + + /** + * Adds a pre-run context handler. + * + * @param handler the context handler + * @return a builder + */ + NumberInputSpec preHandler(Consumer handler); + + /** + * Adds a post-run context handler. + * + * @param handler the context handler + * @return a builder + */ + NumberInputSpec postHandler(Consumer handler); + + /** + * Automatically stores result from a {@link NumberInputContext} into + * {@link ComponentContext} with key given to builder. Defaults to {@code true}. + * + * @param store the flag if storing result + * @return a builder + */ + NumberInputSpec storeResult(boolean store); + + /** + * Define a function which may return id of a next component to go. Returning a + * {@code null} or non existent id indicates that flow should stop. + * + * @param next next component function + * @return a builder + */ + NumberInputSpec next(Function next); + + /** + * Build and return parent builder. + * + * @return the parent builder + */ + Builder and(); +} diff --git a/spring-shell-core/src/main/resources/org/springframework/shell/component/number-input-default.stg b/spring-shell-core/src/main/resources/org/springframework/shell/component/number-input-default.stg new file mode 100644 index 000000000..d7cd2426a --- /dev/null +++ b/spring-shell-core/src/main/resources/org/springframework/shell/component/number-input-default.stg @@ -0,0 +1,45 @@ +// message +message(model) ::= <% + + <({}); format="style-level-error"> + + <({}); format="style-level-warn"> + + <({}); format="style-level-info"> + +%> + +// info section after '? xxx' +info(model) ::= <% + + + + <("[Number Type: "); format="style-value"><("]"); format="style-value"> + + <("[Default "); format="style-value"><("]"); format="style-value"> + + <("[Required]"); format="style-value"> + + +%> + +// start '? xxx' shows both running and result +question_name(model) ::= << +<({}); format="style-list-value"> +>> + +// component result +result(model) ::= << + +>> + +// component is running +running(model) ::= << + + +>> + +// main +main(model) ::= << + +>> diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/NumberInputTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/NumberInputTests.java new file mode 100644 index 000000000..3e3502e12 --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/NumberInputTests.java @@ -0,0 +1,288 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +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.jline.terminal.impl.DumbTerminal; +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.NumberInput.NumberInputContext; +import org.springframework.shell.component.context.ComponentContext; + +public class NumberInputTests extends AbstractShellTests { + + private ExecutorService service; + private CountDownLatch latch1; + private CountDownLatch latch2; + private AtomicReference result1; + private AtomicReference result2; + + @BeforeEach + public void setupTests() { + service = Executors.newFixedThreadPool(1); + latch1 = new CountDownLatch(1); + latch2 = new CountDownLatch(1); + result1 = new AtomicReference<>(); + result2 = new AtomicReference<>(); + } + + @AfterEach + public void cleanupTests() { + latch1 = null; + latch2 = null; + result1 = null; + result2 = null; + if (service != null) { + service.shutdown(); + } + service = null; + } + + @Test + void testNoTty() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8); + + ComponentContext empty = ComponentContext.empty(); + NumberInput component1 = new NumberInput(dumbTerminal, "component1", 100, Double.class); + component1.setPrintResults(true); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + + service.execute(() -> { + NumberInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + NumberInputContext run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isNull(); + } + + @Test + public void testResultBasic() throws InterruptedException { + ComponentContext empty = ComponentContext.empty(); + NumberInput component1 = new NumberInput(getTerminal(), "component1", 100); + component1.setPrintResults(true); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + + service.execute(() -> { + NumberInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + NumberInputContext run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isEqualTo(100); + assertThat(consoleOut()).contains("component1 100"); + } + + @Test + public void testResultBasicWithType() throws InterruptedException { + ComponentContext empty = ComponentContext.empty(); + NumberInput component1 = new NumberInput(getTerminal(), "component1", 50.1, Float.class); + component1.setPrintResults(true); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + + service.execute(() -> { + NumberInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + NumberInputContext run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isEqualTo(50.1); + assertThat(consoleOut()).contains("component1 50.1"); + } + + @Test + public void testResultUserInput() throws InterruptedException { + ComponentContext empty = ComponentContext.empty(); + NumberInput component1 = new NumberInput(getTerminal(), "component1"); + component1.setNumberClass(Double.class); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + + service.execute(() -> { + NumberInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().append("123.3").cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + NumberInputContext run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isEqualTo(123.3d); + } + + @Test + public void testPassingViaContext() throws InterruptedException { + ComponentContext empty = ComponentContext.empty(); + NumberInput component1 = new NumberInput(getTerminal(), "component1", 1); + NumberInput component2 = new NumberInput(getTerminal(), "component2", 2); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + component2.setResourceLoader(new DefaultResourceLoader()); + component2.setTemplateExecutor(getTemplateExecutor()); + + component1.addPostRunHandler(context -> { + context.put(1, context.getResultValue()); + }); + + component2.addPreRunHandler(context -> { + Integer component1ResultValue = context.get(1); + context.setDefaultValue(component1ResultValue); + }); + component2.addPostRunHandler(context -> { + }); + + service.execute(() -> { + NumberInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + + service.execute(() -> { + NumberInputContext run1Context = result1.get(); + NumberInputContext run2Context = component2.run(run1Context); + result2.set(run2Context); + latch2.countDown(); + }); + + write(testBuffer.getBytes()); + + latch2.await(2, TimeUnit.SECONDS); + + NumberInputContext run1Context = result1.get(); + NumberInputContext run2Context = result2.get(); + + assertThat(run1Context).isNotSameAs(run2Context); + + assertThat(run1Context).isNotNull(); + assertThat(run2Context).isNotNull(); + assertThat(run1Context.getResultValue()).isEqualTo(1); + assertThat(run2Context.getResultValue()).isEqualTo(1); + } + + @Test + public void testResultUserInputInvalidInput() throws InterruptedException, IOException { + ComponentContext empty = ComponentContext.empty(); + NumberInput component1 = new NumberInput(getTerminal(), "component1"); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + + service.execute(() -> { + NumberInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().append("x").cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + + NumberInputContext run1Context = result1.get(); + assertThat(consoleOut()).contains("input is invalid"); + assertThat(run1Context).isNull(); + + // backspace 2 : cr + input + testBuffer.backspace(2).append("2").cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + + run1Context = result1.get(); + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isEqualTo(2); + } + + @Test + public void testResultMandatoryInput() throws InterruptedException { + ComponentContext empty = ComponentContext.empty(); + NumberInput component1 = new NumberInput(getTerminal()); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + component1.setRequired(true); + + service.execute(() -> { + NumberInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + + NumberInputContext run1Context = result1.get(); + assertThat(consoleOut()).contains("This field is mandatory"); + assertThat(run1Context).isNull(); + + testBuffer.append("2").cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isEqualTo(2); + } +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java index e58e2e70d..3fcb13413 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.component.flow; +import static org.assertj.core.api.Assertions.assertThat; + import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; @@ -27,11 +29,8 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; - import org.springframework.shell.component.flow.ComponentFlow.ComponentFlowResult; -import static org.assertj.core.api.Assertions.assertThat; - public class ComponentFlowTests extends AbstractShellTests { @Test @@ -52,6 +51,18 @@ public void testSimpleFlow() throws InterruptedException { .withStringInput("field2") .name("Field2") .and() + .withNumberInput("number1") + .name("Number1") + .and() + .withNumberInput("number2") + .name("Number2") + .defaultValue(20.5) + .numberClass(Double.class) + .and() + .withNumberInput("number3") + .name("Number3") + .required() + .and() .withPathInput("path1") .name("Path1") .and() @@ -80,6 +91,15 @@ public void testSimpleFlow() throws InterruptedException { // field2 testBuffer = new TestBuffer().append("Field2Value").cr(); write(testBuffer.getBytes()); + // number1 + testBuffer = new TestBuffer().append("35").cr(); + write(testBuffer.getBytes()); + // number2 + testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + // number3 + testBuffer = new TestBuffer().cr().append("5").cr(); + write(testBuffer.getBytes()); // path1 testBuffer = new TestBuffer().append("fakedir").cr(); write(testBuffer.getBytes()); @@ -95,11 +115,17 @@ public void testSimpleFlow() throws InterruptedException { assertThat(inputWizardResult).isNotNull(); String field1 = inputWizardResult.getContext().get("field1"); String field2 = inputWizardResult.getContext().get("field2"); + Integer number1 = inputWizardResult.getContext().get("number1"); + Double number2 = inputWizardResult.getContext().get("number2"); + Integer number3 = inputWizardResult.getContext().get("number3"); Path path1 = inputWizardResult.getContext().get("path1"); String single1 = inputWizardResult.getContext().get("single1"); List multi1 = inputWizardResult.getContext().get("multi1"); assertThat(field1).isEqualTo("defaultField1Value"); assertThat(field2).isEqualTo("Field2Value"); + assertThat(number1).isEqualTo(35); + assertThat(number2).isEqualTo(20.5); + assertThat(number3).isEqualTo(5); assertThat(path1.toString()).contains("fakedir"); assertThat(single1).isEqualTo("value1"); assertThat(multi1).containsExactlyInAnyOrder("value2"); @@ -132,6 +158,10 @@ public void testSkipsGivenComponents() throws InterruptedException { .resultValue(false) .resultMode(ResultMode.ACCEPT) .and() + .withNumberInput("id6") + .resultValue(50) + .resultMode(ResultMode.ACCEPT) + .and() .build(); ExecutorService service = Executors.newFixedThreadPool(1); @@ -152,12 +182,14 @@ public void testSkipsGivenComponents() throws InterruptedException { String id3 = inputWizardResult.getContext().get("id3"); List id4 = inputWizardResult.getContext().get("id4"); Boolean id5 = inputWizardResult.getContext().get("id5"); + Integer id6 = inputWizardResult.getContext().get("id6"); assertThat(id1).isEqualTo("value1"); assertThat(id2.toString()).contains("value2"); assertThat(id3).isEqualTo("value3"); assertThat(id4).containsExactlyInAnyOrder("value4"); assertThat(id5).isFalse(); + assertThat(id6).isEqualTo(50); } @Test diff --git a/spring-shell-docs/readme.txt b/spring-shell-docs/readme.txt index 34e9a673e..bc4ca97ed 100644 --- a/spring-shell-docs/readme.txt +++ b/spring-shell-docs/readme.txt @@ -1,6 +1,7 @@ Some info how screen recordings were made asciinema rec spring-shell-docs/src/main/asciidoc/asciinema/component-text-input-1.cast +asciinema rec spring-shell-docs/src/main/asciidoc/asciinema/component-number-input-1.cast asciinema rec spring-shell-docs/src/main/asciidoc/asciinema/component-path-input-1.cast asciinema rec spring-shell-docs/src/main/asciidoc/asciinema/component-confirmation-1.cast asciinema rec spring-shell-docs/src/main/asciidoc/asciinema/component-single-select-1.cast @@ -11,6 +12,9 @@ asciinema rec spring-shell-docs/src/main/asciidoc/asciinema/component-flow-condi svg-term \ --in spring-shell-docs/src/main/asciidoc/asciinema/component-text-input-1.cast \ --out spring-shell-docs/src/main/asciidoc/images/component-text-input-1.svg +svg-term \ + --in spring-shell-docs/src/main/asciidoc/asciinema/component-number-input-1.cast \ + --out spring-shell-docs/src/main/asciidoc/images/component-number-input-1.svg svg-term \ --in spring-shell-docs/src/main/asciidoc/asciinema/component-path-input-1.cast \ --out spring-shell-docs/src/main/asciidoc/images/component-path-input-1.svg diff --git a/spring-shell-docs/src/main/asciidoc/asciinema/component-flow-showcase-1.cast b/spring-shell-docs/src/main/asciidoc/asciinema/component-flow-showcase-1.cast index 354bd5c8e..5870b551e 100644 --- a/spring-shell-docs/src/main/asciidoc/asciinema/component-flow-showcase-1.cast +++ b/spring-shell-docs/src/main/asciidoc/asciinema/component-flow-showcase-1.cast @@ -1,42 +1,68 @@ -{"version": 2, "width": 85, "height": 15, "timestamp": 1645645867, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}} -[1.590847, "o", "java -jar spring-shell-samples/target/spring-shell-samples-2.1.0-SNAPSHOT.jar"] -[5.968022, "o", "\r\n"] -[8.099727, "o", "\u001b[?1h\u001b=\u001b[?2004h\u001b[33mmy-shell:>\u001b[0m"] -[11.261894, "o", "\u001b[1mflow showcase\u001b[0m"] -[12.6451, "o", "\r\r\n\u001b[?1l\u001b>\u001b[?1000l\u001b[?2004l"] -[12.721206, "o", "\u001b[?1h\u001b=\u001b[?25l"] -[12.780227, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField1\u001b[0m \u001b[34m[Default defaultField1Value]\u001b[0m\r"] -[14.703281, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K"] -[14.711232, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField1\u001b[0m \u001b[34mdefaultField1Value\u001b[0m\r\n"] -[14.713588, "o", "\u001b[?1h\u001b=\u001b[?25l"] -[14.720745, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField2\u001b[0m \r"] -[16.084492, "o", "\u001b[9Ch\r"] -[16.176942, "o", "\u001b[10Ci\r"] -[16.620009, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K"] -[16.625711, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField2\u001b[0m \u001b[34mhi\u001b[0m\r\n"] -[16.628041, "o", "\u001b[?1h\u001b=\u001b[?25l"] -[16.633919, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mConfirmation1\u001b[0m \u001b[2m(Y/n)\u001b[0m\r"] -[20.090558, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K"] -[20.098654, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mConfirmation1\u001b[0m \u001b[34mtrue\u001b[0m\r\n"] -[20.101548, "o", "\u001b[?1h\u001b=\u001b[?25l"] -[20.105099, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mPath1\u001b[0m \r"] -[21.53213, "o", "\u001b[8Cp\u001b[134C \u001b[32m>\u001b[0m \u001b[32mPath ok\u001b[0m\u001b[A\r"] -[21.626297, "o", "\u001b[9Ca\r"] -[21.896442, "o", "\u001b[10Ct\r"] -[21.941151, "o", "\u001b[11Ch\r"] -[23.041717, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K\r\r\n\u001b[K\u001b[A"] -[23.044506, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mPath1\u001b[0m \u001b[34mpath\u001b[0m\r\n"] -[23.049225, "o", "\u001b[?1h\u001b=\u001b[?25l"] -[23.05459, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mSingle1\u001b[0m [Use arrows to move], type to filter\u001b[97C \u001b[96;1m>\u001b[0m\u001b[96;1m key1\u001b[0m\u001b[137C key2\u001b[2A\r"] -[24.520415, "o", "\r\r\n key1\r\r\n\u001b[96;1m> key2\u001b[0m\u001b[2A\r"] -[25.431855, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K\r\r\n\u001b[K\r\r\n\u001b[K\u001b[2A"] -[25.438619, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mSingle1\u001b[0m \u001b[34mvalue2\u001b[0m\r\n"] -[25.441975, "o", "\u001b[?1h\u001b=\u001b[?25l"] -[25.446547, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mMulti1\u001b[0m [Use arrows to move], type to filter\u001b[98C \u001b[96;1m>\u001b[0m\u001b[96;1m \u001b[39m[ ]\u001b[0m key1\u001b[133C \u001b[1m[ ]\u001b[0m key2\u001b[133C \u001b[1m[ ]\u001b[0m key3\u001b[3A\r"] -[26.698294, "o", "\r\r\n\u001b[2C\u001b[32m[x]\u001b[0m\u001b[A\r"] -[28.022787, "o", "\r\r\n \r\r\n\u001b[96;1m> \u001b[0m\u001b[2A\r"] -[28.110824, "o", "\r\r\n\r\n\u001b[2C\u001b[32m[x]\u001b[0m\u001b[2A\r"] -[29.250629, "o", "\r\r\n\r\n\u001b[2C\u001b[1m[ ]\u001b[0m\u001b[2A\r"] -[30.360368, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K\r\r\n\u001b[K\r\r\n\u001b[K\r\r\n\u001b[K\u001b[3A"] -[30.368273, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mMulti1\u001b[0m \u001b[34mvalue1\u001b[0m\r\n"] -[30.370638, "o", "\u001b[?1h\u001b=\u001b[?2004h\u001b[33mmy-shell:>\u001b[0m"] +{"version":2,"width":85,"height":13,"timestamp":1691402365,"env":{"TERM":"ms-terminal","SHELL":"powershell.exe"}} +[1.1047019958496094,"o","\u001b[25l\u001b[m\u001b[93m\u001b[jjava\u001b[m \u001b[90m-jar\u001b[m spring-shell-samples/target/spring-shell-samples-2.1.13-SNAPSHOT.jar\u001b[?25h"] +[1.5809330940246582,"o","\r\n"] +[4.359606981277466,"o","\u001b[?2004h\u001b[33mmy-shell:>"] +[4.368336200714111,"o","\u001b[m"] +[5.008507013320923,"o","\u001b[31mf"] +[5.020178556442261,"o","\u001b[m"] +[5.110008478164673,"o","\u001b[31ml"] +[5.128786325454712,"o","\u001b[m"] +[5.294262886047363,"o","\u001b[31mo"] +[5.316054582595825,"o","\u001b[m"] +[5.717259883880615,"o","\u001b[31mw"] +[5.738360643386841,"o","\u001b[m"] +[5.893903732299805,"o","\u001b[31m "] +[5.909526586532593,"o","\u001b[m"] +[6.101584434509277,"o","\u001b[31ms"] +[6.112699270248413,"o","\u001b[m"] +[6.213729619979858,"o","\u001b[31mh"] +[6.2375335693359375,"o","\u001b[m"] +[6.30126953125,"o","\u001b[31mo"] +[6.313809156417847,"o","\u001b[m"] +[6.7899205684661865,"o","\u001b[31mw"] +[6.8119776248931885,"o","\u001b[m"] +[7.117271423339844,"o","\u001b[31mc"] +[7.125688552856445,"o","\u001b[m"] +[7.317806005477905,"o","\u001b[31ma"] +[7.330649375915527,"o","\u001b[m"] +[7.413134574890137,"o","\u001b[31ms"] +[7.423953533172607,"o","\u001b[m"] +[7.597413063049316,"o","\u001b[31me"] +[7.613054275512695,"o","\u001b[m"] +[7.966388940811157,"o","\u001b[25l\u001b[2;11H\u001b[?25h"] +[7.975397825241089,"o","\u001b[1m\u001b[97mflow showcase1\u001b[m\u001b[K"] +[8.303815603256226,"o","\r"] +[8.32024359703064,"o","\u001b[?2004l\r\n"] +[8.385269403457642,"o","\u001b[?25l"] +[8.439088344573975,"o","\u001b[1m\u001b[92m?\u001b[m \u001b[1m\u001b[97mField1\u001b[m \u001b[34m[Default defaultField1Value]"] +[8.460672616958618,"o","\u001b[m"] +[9.442008256912231,"o","\u001b[1m\u001b[92m\r?\u001b[m \u001b[1m\u001b[97mField1\u001b[m \u001b[34mdefaultField1Value\u001b[m\u001b[K\u001b[1m\u001b[92m\r\n?\u001b[m \u001b[1m\u001b[97mField2\u001b[m \u001b[K\u001b[200C"] +[10.610269784927368,"o","\u001b[4;10Hh"] +[10.767188787460327,"o","i"] +[11.911431312561035,"o","\u001b[1m\u001b[92m\r?\u001b[m \u001b[1m\u001b[97mField2\u001b[m \u001b[34mhi\u001b[m\u001b[K\u001b[1m\u001b[92m\r\n?\u001b[m \u001b[1m\u001b[97mNumber1\u001b[m \u001b[34m[Number Type: Integer]\u001b[m\u001b[K\u001b[177C"] +[13.872986078262329,"o","\u001b[5;11H5\u001b[176X\u001b[176C\u001b[K\u001b[22C"] +[14.3775053024292,"o","\u001b[1m\u001b[92m\r?\u001b[m \u001b[1m\u001b[97mNumber1\u001b[m 5.\u001b[176X\u001b[176C\u001b[K\u001b[31m\r\n✖\u001b[m \u001b[31mSorry, your input is invalid: '5.', try again\u001b[m\u001b[K\u001b[162C"] +[15.164267301559448,"o","\u001b[5;13H5\u001b[176X\u001b[176C\u001b[K\u001b[31m\u001b[6;13Hr input is invalid: '5.5', try again\u001b[m\u001b[K\u001b[161C"] +[16.089154958724976,"o","\u001b[5;13H\u001b[176X\u001b[176C\u001b[K\u001b[31m\u001b[6;13Hr input is invalid: '5.', try again\u001b[m\u001b[K\u001b[162C"] +[16.401957750320435,"o","\u001b[1m\u001b[92m\u001b[5;1H?\u001b[m \u001b[1m\u001b[97mNumber1\u001b[m 5\u001b[176X\u001b[176C\u001b[K\r\n\u001b[K\u001b[209C"] +[17.43408179283142,"o","\u001b[1m\u001b[92m\u001b[5;1H?\u001b[m \u001b[1m\u001b[97mNumber1\u001b[m \u001b[34m5\u001b[m\u001b[K\u001b[1m\u001b[92m\r\n?\u001b[m \u001b[1m\u001b[97mNumber2\u001b[m \u001b[34m[Number Type: Double][Default \u001b[m20.5\u001b[34m]\u001b[m\u001b[K\u001b[164C"] +[19.153944492340088,"o","\u001b[1m\u001b[92m\r?\u001b[m \u001b[1m\u001b[97mNumber2\u001b[m \u001b[34m20.5\u001b[m\u001b[K\u001b[1m\u001b[92m\r\n?\u001b[m \u001b[1m\u001b[97mConfirmation1\u001b[m (Y/n) \u001b[K\u001b[187C"] +[21.184024572372437,"o","\u001b[1m\u001b[92m\r?\u001b[m \u001b[1m\u001b[97mConfirmation1\u001b[m \u001b[34mtrue\u001b[m\u001b[K\u001b[1m\u001b[92m\r\n?\u001b[m \u001b[1m\u001b[97mPath1\u001b[m \u001b[K\u001b[201C"] +[22.48876166343689,"o","\u001b[1m\u001b[92m\r?\u001b[m \u001b[1m\u001b[97mPath1\u001b[m p\u001b[K\u001b[32m\r\nℹ\u001b[m \u001b[32mPath ok\u001b[m\u001b[K\u001b[200C"] +[22.628902673721313,"o","\u001b[8;10Ha\u001b[K\u001b[199C"] +[22.83142924308777,"o","\u001b[8;11Ht\u001b[K\u001b[198C"] +[22.9888756275177,"o","\u001b[8;12Hh\u001b[K\u001b[197C"] +[24.351086616516113,"o","\u001b[1m\u001b[92m\r?\u001b[m \u001b[1m\u001b[97mPath1\u001b[m \u001b[34mpath\u001b[m\u001b[K\u001b[1m\u001b[92m\r\n?\u001b[m \u001b[1m\u001b[97mSingle1\u001b[m [Use arrows to move], type to filter\u001b[K\u001b[1m\u001b[96m\r\n❯ key1\u001b[m\u001b[K\r\n key2\u001b[K\u001b[203C"] +[25.801196813583374,"o","\u001b[10;1H key1\u001b[1m\u001b[96m\r\n❯ key2"] +[27.1348237991333,"o","\u001b[m"] +[27.156653881072998,"o","\u001b[1m\u001b[92m\u001b[9;1H?\u001b[m \u001b[1m\u001b[97mSingle1\u001b[m \u001b[34mvalue2\u001b[m\u001b[K\u001b[1m\u001b[92m\r\n?\u001b[m \u001b[1m\u001b[97mMulti1\u001b[m [Use arrows to move], type to filter\u001b[K\u001b[1m\u001b[96m\r\n❯ \u001b[97m☐ \u001b[m key1\u001b[K\r\n \u001b[1m\u001b[97m☐ \u001b[m key2\u001b[K\r\n \u001b[1m\u001b[97m☐ \u001b[m key3\u001b[K\u001b[200C"] +[28.298755884170532,"o","\u001b[32m\u001b[11;3H☒ "] +[28.935486793518066,"o","\u001b[m"] +[28.9540057182312,"o","\r \u001b[1m\u001b[96m\r\n❯ "] +[29.616623163223267,"o","\u001b[m"] +[29.6264967918396,"o","\u001b[32m☒ "] +[30.775631189346313,"o","\u001b[m"] +[30.79448390007019,"o","\u001b[1m\u001b[97m\u001b[12;3H☐ "] +[31.608378648757935,"o","\u001b[m"] +[31.622425317764282,"o","\u001b[?2004h\u001b[1m\u001b[92m\u001b[10;1H?\u001b[m \u001b[1m\u001b[97mMulti1\u001b[m \u001b[34mvalue1\u001b[m\u001b[K\u001b[33m\r\nmy-shell:>\u001b[m\u001b[K\r\n\u001b[K\r\n\u001b[K\u001b[11;11H\u001b[?25h"] +[33.2701370716095,"o","\u001b[31m"] diff --git a/spring-shell-docs/src/main/asciidoc/asciinema/component-number-input-1.cast b/spring-shell-docs/src/main/asciidoc/asciinema/component-number-input-1.cast new file mode 100644 index 000000000..584fbc1c9 --- /dev/null +++ b/spring-shell-docs/src/main/asciidoc/asciinema/component-number-input-1.cast @@ -0,0 +1,55 @@ +{"version":2,"width":120,"height":9,"timestamp":1691400709,"env":{"TERM":"ms-terminal","SHELL":"powershell.exe"}} +[1.1047019958496094,"o","\u001b[25l\u001b[m\u001b[93m\u001b[jjava\u001b[m \u001b[90m-jar\u001b[m spring-shell-samples/target/spring-shell-samples-2.1.13-SNAPSHOT.jar\u001b[?25h"] +[1.7505860328674316,"o","\r\n"] +[4.510409593582153,"o","\u001b[?2004h\u001b[33mmy-shell:>"] +[4.519921541213989,"o","\u001b[m"] +[5.577335357666016,"o","\u001b[31mc"] +[5.591983318328857,"o","\u001b[m"] +[5.846746921539307,"o","\u001b[31mo"] +[5.857876777648926,"o","\u001b[m"] +[5.96783971786499,"o","\u001b[31mm"] +[5.98032808303833,"o","\u001b[m"] +[6.270904302597046,"o","\u001b[31mp"] +[6.293165922164917,"o","\u001b[m"] +[6.335582733154297,"o","\u001b[31mo"] +[6.355441093444824,"o","\u001b[m"] +[6.671180009841919,"o","\u001b[31mn"] +[6.684344053268433,"o","\u001b[m"] +[6.855102300643921,"o","\u001b[31me"] +[6.872071743011475,"o","\u001b[m"] +[6.998976230621338,"o","\u001b[31mn"] +[7.0131447315216064,"o","\u001b[m"] +[7.159230947494507,"o","\u001b[31mt"] +[7.1707377433776855,"o","\u001b[m"] +[7.230209112167358,"o","\u001b[31m "] +[7.248788356781006,"o","\u001b[m"] +[7.406573534011841,"o","\u001b[31mn"] +[7.420743227005005,"o","\u001b[m"] +[7.679130554199219,"o","\u001b[31mu"] +[7.699283123016357,"o","\u001b[m"] +[8.135498523712158,"o","\u001b[31mm"] +[8.153958559036255,"o","\u001b[m"] +[8.374905347824097,"o","\u001b[31mb"] +[8.38698434829712,"o","\u001b[m"] +[8.527199268341064,"o","\u001b[31me"] +[8.543792724609375,"o","\u001b[m"] +[8.719360589981079,"o","\u001b[25l\u001b[2;11H\u001b[?25h"] +[8.731481313705444,"o","\u001b[1m\u001b[97mcomponent number\u001b[m\u001b[K"] +[8.814936637878418,"o"," "] +[8.982696294784546,"o","d"] +[9.087361097335815,"o","o"] +[9.151376247406006,"o","u"] +[9.383987665176392,"o","b"] +[9.574780225753784,"o","l"] +[9.70450234413147,"o","\u001b[25l\u001b[2;27H\u001b[?25h"] +[9.718122243881226,"o","\u001b[1m\u001b[97m double\u001b[m\u001b[K"] +[9.928346157073975,"o","\r"] +[9.95093560218811,"o","\u001b[?2004l\r\n"] +[10.007387638092041,"o","\u001b[?25l"] +[10.075207471847534,"o","\u001b[1m\u001b[92m?\u001b[m \u001b[1m\u001b[97mEnter value\u001b[m \u001b[34m[Number Type: Double][Default \u001b[m99.9\u001b[34m]"] +[10.091434478759766,"o","\u001b[m"] +[12.732241153717041,"o","\u001b[3;15H5\u001b[70X\u001b[70C\u001b[K\u001b[35C"] +[13.092545509338379,"o","\u001b[3;16H.\u001b[70X\u001b[70C\u001b[K\u001b[34C"] +[13.577228307723999,"o","\u001b[3;17H5\u001b[70X\u001b[70C\u001b[K\u001b[33C"] +[14.40083646774292,"o","\u001b[?2004h\u001b[1m\u001b[92m\r?\u001b[m \u001b[1m\u001b[97mEnter value\u001b[m \u001b[34m5.5\u001b[m\u001b[K\r\nGot value 5.5\u001b[K\u001b[33m\r\nmy-shell:>\u001b[m\u001b[K\u001b[?25h"] +[15.966568946838379,"o","\u001b[31m"] diff --git a/spring-shell-docs/src/main/asciidoc/images/component-flow-showcase-1.svg b/spring-shell-docs/src/main/asciidoc/images/component-flow-showcase-1.svg index 23363bb4a..d7fcfb84f 100644 --- a/spring-shell-docs/src/main/asciidoc/images/component-flow-showcase-1.svg +++ b/spring-shell-docs/src/main/asciidoc/images/component-flow-showcase-1.svg @@ -1 +1 @@ -java-jarspring-shell-samples/target/spring-shell-samples-2.1.0-SNAPSHOT.jarmy-shell:>my-shell:>flowshowcase?Field1defaultField1Value?Field2hi?Confirmation1true>Pathok?Path1path?Single1[Usearrowstomove],typetofilter?Single1value2?Multi1[Usearrowstomove],typetofilter[]key2[]key3[x]key1>[]key2?Multi1value1?Field1[DefaultdefaultField1Value]?Field2?Field2h?Field2hi?Confirmation1(Y/n)?Path1?Path1p?Path1pa?Path1pat?Path1path>key1key2key1>key2>[]key1>[x]key1>[x]key2 \ No newline at end of file +java-jarspring-shell-samples/target/spring-shell-samples-2.1.13-SNAPSHOT.jarmy-shell:>my-shell:>fmy-shell:>flmy-shell:>flomy-shell:>flowmy-shell:>flowsmy-shell:>flowshmy-shell:>flowshomy-shell:>flowshowmy-shell:>flowshowcmy-shell:>flowshowcamy-shell:>flowshowcasmy-shell:>flowshowcasemy-shell:>flowshowcase1?Field1[DefaultdefaultField1Value]?Field1defaultField1Value?Field2hi?Number15?Number15.Sorry,yourinputisinvalid:'5.',tryagain?Number15?Number220.5?Confirmation1truePathok?Path1path?Single1[Usearrowstomove],typetofilterkey1key2?Single1value2?Multi1[Usearrowstomove],typetofilterkey2key3key1key1key2key2?Multi1value1?Field2?Field2h?Field2hi?Number1[NumberType:Integer]?Number15.5Sorry,yourinputisinvalid:'5.5',tryagain?Number2[NumberType:Double][Default20.5]?Confirmation1(Y/n)?Path1?Path1p?Path1pa?Path1pat?Path1pathkey1key2key1 \ No newline at end of file diff --git a/spring-shell-docs/src/main/asciidoc/images/component-number-input-1.svg b/spring-shell-docs/src/main/asciidoc/images/component-number-input-1.svg new file mode 100644 index 000000000..146fbef02 --- /dev/null +++ b/spring-shell-docs/src/main/asciidoc/images/component-number-input-1.svg @@ -0,0 +1 @@ +java-jarspring-shell-samples/target/spring-shell-samples-2.1.13-SNAPSHOT.jarmy-shell:>my-shell:>cmy-shell:>comy-shell:>commy-shell:>compmy-shell:>compomy-shell:>componmy-shell:>componemy-shell:>componenmy-shell:>componentmy-shell:>componentnmy-shell:>componentnumy-shell:>componentnummy-shell:>componentnumbmy-shell:>componentnumbemy-shell:>componentnumbermy-shell:>componentnumberdoublmy-shell:>componentnumberdouble?Entervalue[NumberType:Double][Default99.9]?Entervalue5.5Gotvalue5.5my-shell:>componentnumberdmy-shell:>componentnumberdomy-shell:>componentnumberdoumy-shell:>componentnumberdoub?Entervalue5?Entervalue5.?Entervalue5.5 \ No newline at end of file diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-components-flow.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-components-flow.adoc index 5faf17649..a1eb25a36 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-components-flow.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-components-flow.adoc @@ -21,7 +21,7 @@ image::images/component-flow-showcase-1.svg[text input] Normal execution order of a components is same as defined with a builder. It's possible to conditionally choose where to jump in a flow by using a `next` -function and returning target _component id_. If this returned id is aither _null_ +function and returning target _component id_. If this returned id is either _null_ or doesn't exist flow is essentially stopped right there. ==== diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-components-ui-numberinput.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-components-ui-numberinput.adoc new file mode 100644 index 000000000..ddeac010e --- /dev/null +++ b/spring-shell-docs/src/main/asciidoc/using-shell-components-ui-numberinput.adoc @@ -0,0 +1,36 @@ +[[using-shell-components-ui-numberinput]] +==== Number Input +ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs] + +The number input component asks a user for simple number input. It can be configured to use any implementation of Number.class. The following listing shows an example: + +==== +[source, java, indent=0] +---- +include::{snippets}/UiComponentSnippets.java[tag=snippet9] +---- +==== + +The following image shows typical output from a number input component: + +image::images/component-number-input-1.svg[text input] + +The context object is `NumberInputContext`. The following table lists its context variables: + +[[numberinputcontext-template-variables]] +.NumberInputContext Template Variables +|=== +|Key |Description + +|`defaultValue` +|The default value, if set. Otherwise, null. + +|`defaultClass` +|The default number class to use, if set. Otherwise, Integer.class. + +|`required` +|`true` if the input is required. Otherwise, false. + +|`model` +|The parent context variables (see <>). +|=== diff --git a/spring-shell-docs/src/main/asciidoc/using-shell-components-ui.adoc b/spring-shell-docs/src/main/asciidoc/using-shell-components-ui.adoc index 5fc424409..67f745ef5 100644 --- a/spring-shell-docs/src/main/asciidoc/using-shell-components-ui.adoc +++ b/spring-shell-docs/src/main/asciidoc/using-shell-components-ui.adoc @@ -25,6 +25,8 @@ include::using-shell-components-ui-render.adoc[] include::using-shell-components-ui-stringinput.adoc[] +include::using-shell-components-ui-numberinput.adoc[] + include::using-shell-components-ui-pathinput.adoc[] include::using-shell-components-ui-confirmation.adoc[] diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/FlowComponentSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/FlowComponentSnippets.java index 1a26d5ed5..b7e504ffa 100644 --- a/spring-shell-docs/src/test/java/org/springframework/shell/docs/FlowComponentSnippets.java +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/FlowComponentSnippets.java @@ -46,6 +46,14 @@ public void runFlow() { .withStringInput("field2") .name("Field2") .and() + .withNumberInput("number1") + .name("Number1") + .and() + .withNumberInput("number2") + .name("Number2") + .defaultValue(20.5) + .numberClass(Double.class) + .and() .withConfirmationInput("confirmation1") .name("Confirmation1") .and() diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/UiComponentSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/UiComponentSnippets.java index 88c866d4b..62298ada3 100644 --- a/spring-shell-docs/src/test/java/org/springframework/shell/docs/UiComponentSnippets.java +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/UiComponentSnippets.java @@ -24,16 +24,17 @@ import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; - 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; import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext; +import org.springframework.shell.component.NumberInput; +import org.springframework.shell.component.NumberInput.NumberInputContext; +import org.springframework.shell.component.PathInput; import org.springframework.shell.component.PathInput.PathInputContext; +import org.springframework.shell.component.SingleItemSelector; import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext; +import org.springframework.shell.component.StringInput; import org.springframework.shell.component.StringInput.StringInputContext; import org.springframework.shell.component.support.SelectorItem; import org.springframework.shell.standard.AbstractShellComponent; @@ -211,4 +212,21 @@ public String singleSelector() { } } + class Dump8 { + // tag::snippet9[] + @ShellComponent + public class ComponentCommands extends AbstractShellComponent { + + @ShellMethod(key = "component number", value = "Number input", group = "Components") + public String numberInput() { + NumberInput component = new NumberInput(getTerminal(), "Enter value", 99.9, Double.class); + component.setResourceLoader(getResourceLoader()); + component.setTemplateExecutor(getTemplateExecutor()); + NumberInputContext context = component.run(NumberInputContext.empty()); + return "Got value " + context.getResultValue(); + } + } + // end::snippet9[] + } + } diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java index a85e994d6..a876d1314 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java @@ -24,11 +24,12 @@ import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; - import org.springframework.shell.component.ConfirmationInput; import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext; import org.springframework.shell.component.MultiItemSelector; import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext; +import org.springframework.shell.component.NumberInput; +import org.springframework.shell.component.NumberInput.NumberInputContext; import org.springframework.shell.component.PathInput; import org.springframework.shell.component.PathInput.PathInputContext; import org.springframework.shell.component.SingleItemSelector; @@ -56,6 +57,34 @@ public String stringInput(boolean mask) { return "Got value " + context.getResultValue(); } + @ShellMethod(key = "component number", value = "Number input", group = "Components") + public String numberInput() { + NumberInput component = new NumberInput(getTerminal(), "Enter value"); + component.setResourceLoader(getResourceLoader()); + component.setTemplateExecutor(getTemplateExecutor()); + NumberInputContext context = component.run(NumberInputContext.empty()); + return "Got value " + context.getResultValue(); + } + + @ShellMethod(key = "component number double", value = "Number double input", group = "Components") + public String numberInputDouble() { + NumberInput component = new NumberInput(getTerminal(), "Enter value", 99.9, Double.class); + component.setResourceLoader(getResourceLoader()); + component.setTemplateExecutor(getTemplateExecutor()); + NumberInputContext context = component.run(NumberInputContext.empty()); + return "Got value " + context.getResultValue(); + } + + @ShellMethod(key = "component number required", value = "Number input", group = "Components") + public String numberInputRequired() { + NumberInput component = new NumberInput(getTerminal(), "Enter value"); + component.setRequired(true); + component.setResourceLoader(getResourceLoader()); + component.setTemplateExecutor(getTemplateExecutor()); + NumberInputContext context = component.run(NumberInputContext.empty()); + return "Got value " + context.getResultValue(); + } + @ShellMethod(key = "component path", value = "Path input", group = "Components") public String pathInput() { PathInput component = new PathInput(getTerminal(), "Enter value"); diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java index 696c1e001..8610e545b 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java @@ -24,7 +24,6 @@ import java.util.stream.IntStream; import org.jline.terminal.impl.DumbTerminal; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException; @@ -62,6 +61,18 @@ public void showcase1() { .withStringInput("field2") .name("Field2") .and() + .withNumberInput("number1") + .name("Number1") + .and() + .withNumberInput("number2") + .name("Number2") + .defaultValue(20.5) + .numberClass(Double.class) + .and() + .withNumberInput("number3") + .name("Field3") + .required() + .and() .withConfirmationInput("confirmation1") .name("Confirmation1") .and() @@ -84,6 +95,8 @@ public void showcase1() { public String showcase2( @ShellOption(help = "Field1 value", defaultValue = ShellOption.NULL) String field1, @ShellOption(help = "Field2 value", defaultValue = ShellOption.NULL) String field2, + @ShellOption(help = "Number1 value", defaultValue = ShellOption.NULL) Integer number1, + @ShellOption(help = "Number2 value", defaultValue = ShellOption.NULL) Double number2, @ShellOption(help = "Confirmation1 value", defaultValue = ShellOption.NULL) Boolean confirmation1, @ShellOption(help = "Path1 value", defaultValue = ShellOption.NULL) String path1, @ShellOption(help = "Single1 value", defaultValue = ShellOption.NULL) String single1, @@ -107,6 +120,17 @@ public String showcase2( .resultValue(field2) .resultMode(ResultMode.ACCEPT) .and() + .withNumberInput("number1") + .name("Number1") + .resultValue(number1) + .resultMode(ResultMode.ACCEPT) + .and() + .withNumberInput("number2") + .name("Number2") + .resultValue(number2) + .numberClass(Double.class) + .resultMode(ResultMode.ACCEPT) + .and() .withConfirmationInput("confirmation1") .name("Confirmation1") .resultValue(confirmation1) @@ -152,6 +176,9 @@ public CommandRegistration showcaseRegistration() { .withOption() .longNames("field2") .and() + .withOption() + .longNames("number1") + .and() .withOption() .longNames("confirmation1") .type(Boolean.class) @@ -170,6 +197,7 @@ public CommandRegistration showcaseRegistration() { String field1 = ctx.getOptionValue("field1"); String field2 = ctx.getOptionValue("field2"); + Integer number1 = ctx.getOptionValue("number1"); Boolean confirmation1 = ctx.getOptionValue("confirmation1"); String path1 = ctx.getOptionValue("path1"); String single1 = ctx.getOptionValue("single1"); @@ -196,6 +224,11 @@ public CommandRegistration showcaseRegistration() { .resultValue(field2) .resultMode(ResultMode.ACCEPT) .and() + .withNumberInput("number1") + .name("Number1") + .resultValue(number1) + .resultMode(ResultMode.ACCEPT) + .and() .withConfirmationInput("confirmation1") .name("Confirmation1") .resultValue(confirmation1)