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..755b1af69 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/NumberInput.java @@ -0,0 +1,322 @@ +/* + * 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.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 b7a663e91..4378acc17 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 3510a38cf..39d70ac88 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; @@ -32,8 +34,6 @@ import org.springframework.shell.component.flow.ComponentFlow.ComponentFlowResult; import org.springframework.test.util.ReflectionTestUtils; -import static org.assertj.core.api.Assertions.assertThat; - public class ComponentFlowTests extends AbstractShellTests { @Test @@ -54,6 +54,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() @@ -82,6 +94,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()); @@ -97,11 +118,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"); @@ -134,6 +161,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); @@ -154,12 +185,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/modules/ROOT/examples/component-number-input-1.cast b/spring-shell-docs/modules/ROOT/examples/component-number-input-1.cast new file mode 100644 index 000000000..8d9d62e7c --- /dev/null +++ b/spring-shell-docs/modules/ROOT/examples/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"] \ No newline at end of file diff --git a/spring-shell-docs/modules/ROOT/nav.adoc b/spring-shell-docs/modules/ROOT/nav.adoc index 154678c79..ea698b651 100644 --- a/spring-shell-docs/modules/ROOT/nav.adoc +++ b/spring-shell-docs/modules/ROOT/nav.adoc @@ -49,6 +49,7 @@ ** xref:components/ui/index.adoc[] *** xref:components/ui/render.adoc[] *** xref:components/ui/stringinput.adoc[] +*** xref:components/ui/numberinput.adoc[] *** xref:components/ui/pathinput.adoc[] *** xref:components/ui/pathsearch.adoc[] *** xref:components/ui/confirmation.adoc[] diff --git a/spring-shell-docs/modules/ROOT/pages/components/ui/numberinput.adoc b/spring-shell-docs/modules/ROOT/pages/components/ui/numberinput.adoc new file mode 100644 index 000000000..741bb059e --- /dev/null +++ b/spring-shell-docs/modules/ROOT/pages/components/ui/numberinput.adoc @@ -0,0 +1,38 @@ +[[using-shell-components-ui-numberinput]] += Number Input + +ifndef::snippets[:snippets: ../../../../../src/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=snippet10] +---- + +The following image shows typical output from a number input component: + +[asciinema,rows=6] +---- +include::example$component-number-input-1.cast[] +---- + +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 xref:components/ui/render.adoc#textcomponentcontext-template-variables[TextComponentContext Template Variables]). +|=== \ No newline at end of file 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 fad1cc23c..1af6582c5 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 @@ -45,6 +45,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 40b0dc29c..326c069ff 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,12 +24,16 @@ 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.PathSearch; 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.MultiItemSelector.MultiItemSelectorContext; @@ -238,4 +242,21 @@ public String pathSearch() { } } } + + class Dump9 { + // tag::snippet10[] + @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::snippet10[] + } } diff --git a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java index 266cdc986..c85820c85 100644 --- a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentCommands.java +++ b/spring-shell-samples/spring-shell-sample-commands/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.PathSearch; import org.springframework.shell.component.PathInput.PathInputContext; @@ -60,6 +61,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 input", value = "Path input", group = "Components") public String pathInput() { PathInput component = new PathInput(getTerminal(), "Enter value"); diff --git a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java index 696c1e001..8610e545b 100644 --- a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java +++ b/spring-shell-samples/spring-shell-sample-commands/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)