diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseSingleItemSelector.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseSingleItemSelector.java index 0b0bebf97..654b46f2e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseSingleItemSelector.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseSingleItemSelector.java @@ -41,6 +41,7 @@ public abstract class BaseSingleItemSelector extends BaseInput selectItems = new HashMap<>(); + private String defaultSelect; private Comparator> comparator; private Function>, List> renderer; private Integer maxItems; @@ -84,6 +85,12 @@ public SingleItemSelectorSpec selectItems(Map selectItems) { return this; } + @Override + public SingleItemSelectorSpec defaultSelect(String name) { + this.defaultSelect = name; + return this; + } + @Override public SingleItemSelectorSpec sort(Comparator> comparator) { this.comparator = comparator; @@ -160,6 +167,10 @@ public Map getSelectItems() { return selectItems; } + public String getDefaultSelect() { + return defaultSelect; + } + public Comparator> getComparator() { return comparator; } 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 6a6440928..cd4d308b4 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 @@ -555,8 +555,20 @@ private Stream singleItemSelectorsStream() { List> selectorItems = input.getSelectItems().entrySet().stream() .map(e -> SelectorItem.of(e.getKey(), e.getValue())) .collect(Collectors.toList()); + + // setup possible item for initial expose + String defaultSelect = input.getDefaultSelect(); + Stream> defaultCheckStream = StringUtils.hasText(defaultSelect) + ? selectorItems.stream() + : Stream.empty(); + SelectorItem defaultExpose = defaultCheckStream + .filter(si -> ObjectUtils.nullSafeEquals(defaultSelect, si.getName())) + .findFirst() + .orElse(null); + SingleItemSelector> selector = new SingleItemSelector<>(terminal, selectorItems, input.getName(), input.getComparator()); + selector.setDefaultExpose(defaultExpose); Function, ComponentContext> operation = (context) -> { if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult() && StringUtils.hasText(input.getResultValue())) { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java index 5dfabdccc..c9ddef3f6 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java @@ -76,6 +76,14 @@ public interface SingleItemSelectorSpec extends BaseInputSpec selectItems); + /** + * Automatically selects and exposes a given item. + * + * @param name the name + * @return a builder + */ + SingleItemSelectorSpec defaultSelect(String name); + /** * Sets a {@link Comparator} for sorting items. * diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java index 3257701e5..9306b4419 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java @@ -31,6 +31,7 @@ import org.springframework.shell.component.context.ComponentContext; import org.springframework.shell.component.support.AbstractSelectorComponent.SelectorComponentContext; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import static org.jline.keymap.KeyMap.ctrl; @@ -54,6 +55,8 @@ public abstract class AbstractSelectorComponent items, boolean exitSelects, Comparator comparator) { @@ -95,6 +98,18 @@ public Function getItemMapper() { return itemMapper; } + /** + * Sets default expose item when component start. + * + * @param defaultExpose the default item + */ + public void setDefaultExpose(I defaultExpose) { + this.defaultExpose = defaultExpose; + if (defaultExpose != null) { + expose = true; + } + } + /** * Gets items. * @@ -120,6 +135,7 @@ protected void bindKeyMap(KeyMap keyMap) { @Override protected C runInternal(C context) { C thisContext = getThisContext(context); + initialExpose(thisContext); ItemStateViewProjection buildItemStateView = buildItemStateView(start.get(), thisContext); List> itemStateView = buildItemStateView.items; thisContext.setItemStateView(itemStateView); @@ -224,6 +240,33 @@ else if (start.get() + pos.get() <= 0) { return false; } + private void initialExpose(C context) { + if (!expose) { + return; + } + expose = false; + List> itemStates = context.getItemStates(); + if (itemStates == null) { + AtomicInteger index = new AtomicInteger(0); + itemStates = context.getItems().stream() + .sorted(comparator) + .map(item -> ItemState.of(item, item.getName(), index.getAndIncrement(), item.isEnabled())) + .collect(Collectors.toList()); + } + for (int i = 0; i < itemStates.size(); i++) { + if (ObjectUtils.nullSafeEquals(itemStates.get(i).getName(), defaultExpose.getName())) { + if (i < maxItems) { + this.pos.set(i); + } + else { + this.pos.set(maxItems - 1); + this.start.set(i - maxItems + 1); + } + break; + } + } + } + private ItemStateViewProjection buildItemStateView(int skip, SelectorComponentContext context) { List> itemStates = context.getItemStates(); if (itemStates == null) { 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 316971585..059f21fa6 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 @@ -154,56 +154,89 @@ public void testSkipsGivenComponents() throws InterruptedException { assertThat(id4).containsExactlyInAnyOrder("value4"); } - @Test - public void testChoosesDynamically() throws InterruptedException { - ComponentFlow wizard = ComponentFlow.builder() + @Test + public void testChoosesDynamically() throws InterruptedException { + ComponentFlow wizard = ComponentFlow.builder() + .terminal(getTerminal()) + .resourceLoader(getResourceLoader()) + .templateExecutor(getTemplateExecutor()) + .withStringInput("id1") + .name("name") + .next(ctx -> ctx.get("id1")) + .and() + .withStringInput("id2") + .name("name") + .resultValue("value2") + .resultMode(ResultMode.ACCEPT) + .next(ctx -> null) + .and() + .withStringInput("id3") + .name("name") + .resultValue("value3") + .resultMode(ResultMode.ACCEPT) + .next(ctx -> null) + .and() + .build(); + + ExecutorService service = Executors.newFixedThreadPool(1); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference result = new AtomicReference<>(); + service.execute(() -> { + result.set(wizard.run()); + latch.countDown(); + }); + + // id1 + TestBuffer testBuffer = new TestBuffer().append("id3").cr(); + write(testBuffer.getBytes()); + // id3 + testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + latch.await(4, TimeUnit.SECONDS); + ComponentFlowResult inputWizardResult = result.get(); + assertThat(inputWizardResult).isNotNull(); + String id1 = inputWizardResult.getContext().get("id1"); + // TODO: should be able to check if variable exists + // String id2 = inputWizardResult.getContext().get("id2"); + String id3 = inputWizardResult.getContext().get("id3"); + assertThat(id1).isEqualTo("id3"); + // assertThat(id2).isNull(); + assertThat(id3).isEqualTo("value3"); + } + + @Test + public void testAutoShowsDefault() throws InterruptedException { + Map single1SelectItems = new HashMap<>(); + single1SelectItems.put("key1", "value1"); + single1SelectItems.put("key2", "value2"); + ComponentFlow wizard = ComponentFlow.builder() .terminal(getTerminal()) .resourceLoader(getResourceLoader()) .templateExecutor(getTemplateExecutor()) - .withStringInput("id1") - .name("name") - .next(ctx -> ctx.get("id1")) - .and() - .withStringInput("id2") - .name("name") - .resultValue("value2") - .resultMode(ResultMode.ACCEPT) - .next(ctx -> null) - .and() - .withStringInput("id3") - .name("name") - .resultValue("value3") - .resultMode(ResultMode.ACCEPT) - .next(ctx -> null) + .withSingleItemSelector("single1") + .name("Single1") + .selectItems(single1SelectItems) + .defaultSelect("key2") .and() .build(); - ExecutorService service = Executors.newFixedThreadPool(1); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference result = new AtomicReference<>(); - - service.execute(() -> { - result.set(wizard.run()); - latch.countDown(); - }); - - // id1 - TestBuffer testBuffer = new TestBuffer().append("id3").cr(); - write(testBuffer.getBytes()); - - // id3 - testBuffer = new TestBuffer().cr(); - write(testBuffer.getBytes()); - - latch.await(4, TimeUnit.SECONDS); - ComponentFlowResult inputWizardResult = result.get(); - assertThat(inputWizardResult).isNotNull(); - String id1 = inputWizardResult.getContext().get("id1"); - // TODO: should be able to check if variable exists - // String id2 = inputWizardResult.getContext().get("id2"); - String id3 = inputWizardResult.getContext().get("id3"); - assertThat(id1).isEqualTo("id3"); - // assertThat(id2).isNull(); - assertThat(id3).isEqualTo("value3"); - } + ExecutorService service = Executors.newFixedThreadPool(1); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference result = new AtomicReference<>(); + + service.execute(() -> { + result.set(wizard.run()); + latch.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer(); + testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch.await(4, TimeUnit.SECONDS); + ComponentFlowResult inputWizardResult = result.get(); + assertThat(inputWizardResult).isNotNull(); + String single1 = inputWizardResult.getContext().get("single1"); + assertThat(single1).isEqualTo("value2"); + } } 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 e3cbddc0e..c28f6d667 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 @@ -19,6 +19,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.shell.component.flow.ComponentFlow; @@ -26,6 +28,7 @@ import org.springframework.shell.standard.AbstractShellComponent; import org.springframework.shell.standard.ShellComponent; import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellOption; @ShellComponent public class ComponentFlowCommands extends AbstractShellComponent { @@ -90,4 +93,23 @@ public void conditional() { .build(); flow.run(); } + + @ShellMethod(key = "flow autoselect", value = "Autoselect item", group = "Flow") + public void autoselect( + @ShellOption(defaultValue = "Field3") String defaultValue + ) { + Map single1SelectItems = IntStream.range(1, 10) + .boxed() + .collect(Collectors.toMap(i -> "Field" + i, i -> "field" + i)); + + ComponentFlow flow = componentFlowBuilder.clone().reset() + .withSingleItemSelector("single1") + .name("Single1") + .selectItems(single1SelectItems) + .defaultSelect(defaultValue) + .sort((o1, o2) -> o1.getName().compareTo(o2.getName())) + .and() + .build(); + flow.run(); + } }