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 777f5d6c9..c33118a27 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 @@ -421,11 +421,19 @@ private DefaultComponentFlowResult runGetResults() { context = node.data.getOperation().apply(context); if (node.data.next != null) { Optional n = node.data.next.apply(context); - if (n.isPresent()) { + if (n == null) { + // actual function returned null optional which is case + // when no next function was set. this is different + // than when null is returned for next. + node = node.next; + } + else if (n.isPresent()) { + // user returned value node = oiol.get(n.get()); } else { - node = node.next; + // user returned null + node = null; } } else { @@ -474,7 +482,7 @@ private Stream stringInputsStream() { Function f1 = input.getNext(); Function, Optional> f2 = context -> f1 != null ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) - : Optional.empty(); + : null; return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); }); } @@ -512,7 +520,7 @@ private Stream pathInputsStream() { Function f1 = input.getNext(); Function, Optional> f2 = context -> f1 != null ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) - : Optional.empty(); + : null; return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); }); } @@ -550,7 +558,7 @@ private Stream confirmationInputsStream() { Function f1 = input.getNext(); Function, Optional> f2 = context -> f1 != null ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) - : Optional.empty(); + : null; return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); }); } @@ -609,7 +617,7 @@ private Stream singleItemSelectorsStream() { Function>, String> f1 = input.getNext(); Function, Optional> f2 = context -> f1 != null ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) - : Optional.empty(); + : null; return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); }); } @@ -654,7 +662,7 @@ private Stream multiItemSelectorsStream() { Function>, String> f1 = input.getNext(); Function, Optional> f2 = context -> f1 != null ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) - : Optional.empty(); + : null; return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); }); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ConfirmationInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ConfirmationInputSpec.java index 2f4b39b91..d7f449012 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ConfirmationInputSpec.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ConfirmationInputSpec.java @@ -105,7 +105,8 @@ public interface ConfirmationInputSpec extends BaseInputSpec { PathInputSpec storeResult(boolean store); /** - * Define a function which may return id of a next component to go. + * 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 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 c9ddef3f6..4c5c4aa9f 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 @@ -142,7 +142,8 @@ public interface SingleItemSelectorSpec extends BaseInputSpec { StringInputSpec storeResult(boolean store); /** - * Define a function which may return id of a next component to go. + * 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 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 e77ed914c..e58e2e70d 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 @@ -161,7 +161,7 @@ public void testSkipsGivenComponents() throws InterruptedException { } @Test - public void testChoosesDynamically() throws InterruptedException { + public void testChoosesDynamicallyShouldJumpOverAndStop() throws InterruptedException { ComponentFlow wizard = ComponentFlow.builder() .terminal(getTerminal()) .resourceLoader(getResourceLoader()) @@ -202,12 +202,106 @@ public void testChoosesDynamically() throws InterruptedException { 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"); + assertThat(inputWizardResult.getContext().containsKey("id2")).isFalse(); + } + + @Test + public void testChoosesDynamicallyShouldNotContinueToNext() 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("id2").cr(); + write(testBuffer.getBytes()); + // id2 + 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"); + String id2 = inputWizardResult.getContext().get("id2"); + assertThat(id1).isEqualTo("id2"); + assertThat(id2).isEqualTo("value2"); + assertThat(inputWizardResult.getContext().containsKey("id3")).isFalse(); + } + + @Test + public void testChoosesNonExistingComponent() 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("fake").cr(); + write(testBuffer.getBytes()); + + // don't execute id2 or 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"); + assertThat(id1).isEqualTo("fake"); + assertThat(inputWizardResult.getContext().containsKey("id2")).isFalse(); + assertThat(inputWizardResult.getContext().containsKey("id3")).isFalse(); } @Test diff --git a/spring-shell-docs/src/main/asciidoc/asciinema/component-flow-conditional-1.cast b/spring-shell-docs/src/main/asciidoc/asciinema/component-flow-conditional-1.cast index 584a4ea1a..600e59e0b 100644 --- a/spring-shell-docs/src/main/asciidoc/asciinema/component-flow-conditional-1.cast +++ b/spring-shell-docs/src/main/asciidoc/asciinema/component-flow-conditional-1.cast @@ -21,9 +21,6 @@ [12.734384, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K"] [12.740754, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField1\u001b[0m \u001b[34mdefaultField1Value\u001b[0m\r\n"] [12.74362, "o", "\u001b[?1h\u001b=\u001b[?25l"] -[12.750944, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField2\u001b[0m \u001b[34m[Default defaultField2Value]\u001b[0m\r"] -[13.542429, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K"] -[13.545672, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField2\u001b[0m \u001b[34mdefaultField2Value\u001b[0m\r\n"] [13.547149, "o", "\u001b[?1h\u001b=\u001b[?2004h\u001b[33mmy-shell:>\u001b[0m"] [14.091367, "o", "\u001b[1mflow conditional\u001b[0m"] [14.419251, "o", "\r\r\n\u001b[?1l\u001b>\u001b[?1000l\u001b[?2004l"] diff --git a/spring-shell-docs/src/main/asciidoc/images/component-flow-conditional-1.svg b/spring-shell-docs/src/main/asciidoc/images/component-flow-conditional-1.svg index a325b4729..9cf2e97d5 100644 --- a/spring-shell-docs/src/main/asciidoc/images/component-flow-conditional-1.svg +++ b/spring-shell-docs/src/main/asciidoc/images/component-flow-conditional-1.svg @@ -1 +1 @@ -java-jarspring-shell-samples/target/spring-shell-samples-2.1.0-SNAPSHOT.jarmy-shell:>my-shell:>flowmy-shell:>flowconditional?Single1[Usearrowstomove],typetofilter>Field1Field2?Single1field1?Field1defaultField1Value?Field2[DefaultdefaultField2Value]?Field2defaultField2Value?Single1field2org.jline.reader.EndOfFileExceptionDetailsoftheerrorhavebeenomitted.Youcanusethestacktracecommandtoprintthefullstacktrace.my-shell:>fmy-shell:>flmy-shell:>flomy-shell:>flowcmy-shell:>flowcomy-shell:>flowcon?Field1[DefaultdefaultField1Value]Field1>Field2 \ No newline at end of file +java-jarspring-shell-samples/target/spring-shell-samples-2.1.0-SNAPSHOT.jarmy-shell:>my-shell:>flowmy-shell:>flowconditional?Single1[Usearrowstomove],typetofilter>Field1Field2?Single1field1?Field1defaultField1Value?Single1field2?Field2defaultField2Valueorg.jline.reader.EndOfFileExceptionDetailsoftheerrorhavebeenomitted.Youcanusethestacktracecommandtoprintthefullstacktrace.my-shell:>fmy-shell:>flmy-shell:>flomy-shell:>flowcmy-shell:>flowcomy-shell:>flowcon?Field1[DefaultdefaultField1Value]Field1>Field2?Field2[DefaultdefaultField2Value] \ 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 a35e30124..5faf17649 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 @@ -19,6 +19,10 @@ include::{snippets}/FlowComponentSnippets.java[tag=snippet1] 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_ +or doesn't exist flow is essentially stopped right there. ==== [source, java, indent=0]