Skip to content

Commit 2cf7123

Browse files
committed
Fix next handling in ComponentFlow
- This fixes a bug where returning null from a next() didn't stop a flow. - Backport #510 - Fixes #513 (cherry picked from commit 7c4700b)
1 parent 9087692 commit 2cf7123

File tree

10 files changed

+128
-20
lines changed

10 files changed

+128
-20
lines changed

spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -421,11 +421,19 @@ private DefaultComponentFlowResult runGetResults() {
421421
context = node.data.getOperation().apply(context);
422422
if (node.data.next != null) {
423423
Optional<String> n = node.data.next.apply(context);
424-
if (n.isPresent()) {
424+
if (n == null) {
425+
// actual function returned null optional which is case
426+
// when no next function was set. this is different
427+
// than when null is returned for next.
428+
node = node.next;
429+
}
430+
else if (n.isPresent()) {
431+
// user returned value
425432
node = oiol.get(n.get());
426433
}
427434
else {
428-
node = node.next;
435+
// user returned null
436+
node = null;
429437
}
430438
}
431439
else {
@@ -474,7 +482,7 @@ private Stream<OrderedInputOperation> stringInputsStream() {
474482
Function<StringInputContext, String> f1 = input.getNext();
475483
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
476484
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
477-
: Optional.empty();
485+
: null;
478486
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
479487
});
480488
}
@@ -512,7 +520,7 @@ private Stream<OrderedInputOperation> pathInputsStream() {
512520
Function<PathInputContext, String> f1 = input.getNext();
513521
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
514522
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
515-
: Optional.empty();
523+
: null;
516524
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
517525
});
518526
}
@@ -550,7 +558,7 @@ private Stream<OrderedInputOperation> confirmationInputsStream() {
550558
Function<ConfirmationInputContext, String> f1 = input.getNext();
551559
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
552560
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
553-
: Optional.empty();
561+
: null;
554562
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
555563
});
556564
}
@@ -609,7 +617,7 @@ private Stream<OrderedInputOperation> singleItemSelectorsStream() {
609617
Function<SingleItemSelectorContext<String, SelectorItem<String>>, String> f1 = input.getNext();
610618
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
611619
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
612-
: Optional.empty();
620+
: null;
613621
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
614622
});
615623
}
@@ -654,7 +662,7 @@ private Stream<OrderedInputOperation> multiItemSelectorsStream() {
654662
Function<MultiItemSelectorContext<String, SelectorItem<String>>, String> f1 = input.getNext();
655663
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
656664
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
657-
: Optional.empty();
665+
: null;
658666
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
659667
});
660668
}

spring-shell-core/src/main/java/org/springframework/shell/component/flow/ConfirmationInputSpec.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ public interface ConfirmationInputSpec extends BaseInputSpec<ConfirmationInputSp
105105
ConfirmationInputSpec storeResult(boolean store);
106106

107107
/**
108-
* Define a function which may return id of a next component to go.
108+
* Define a function which may return id of a next component to go. Returning a
109+
* {@code null} or non existent id indicates that flow should stop.
109110
*
110111
* @param next next component function
111112
* @return a builder

spring-shell-core/src/main/java/org/springframework/shell/component/flow/MultiItemSelectorSpec.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ public interface MultiItemSelectorSpec extends BaseInputSpec<MultiItemSelectorSp
124124
MultiItemSelectorSpec storeResult(boolean store);
125125

126126
/**
127-
* Define a function which may return id of a next component to go.
127+
* Define a function which may return id of a next component to go. Returning a
128+
* {@code null} or non existent id indicates that flow should stop.
128129
*
129130
* @param next next component function
130131
* @return a builder

spring-shell-core/src/main/java/org/springframework/shell/component/flow/PathInputSpec.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ public interface PathInputSpec extends BaseInputSpec<PathInputSpec> {
107107
PathInputSpec storeResult(boolean store);
108108

109109
/**
110-
* Define a function which may return id of a next component to go.
110+
* Define a function which may return id of a next component to go. Returning a
111+
* {@code null} or non existent id indicates that flow should stop.
111112
*
112113
* @param next next component function
113114
* @return a builder

spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ public interface SingleItemSelectorSpec extends BaseInputSpec<SingleItemSelector
142142
SingleItemSelectorSpec storeResult(boolean store);
143143

144144
/**
145-
* Define a function which may return id of a next component to go.
145+
* Define a function which may return id of a next component to go. Returning a
146+
* {@code null} or non existent id indicates that flow should stop.
146147
*
147148
* @param next next component function
148149
* @return a builder

spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ public interface StringInputSpec extends BaseInputSpec<StringInputSpec> {
114114
StringInputSpec storeResult(boolean store);
115115

116116
/**
117-
* Define a function which may return id of a next component to go.
117+
* Define a function which may return id of a next component to go. Returning a
118+
* {@code null} or non existent id indicates that flow should stop.
118119
*
119120
* @param next next component function
120121
* @return a builder

spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public void testSkipsGivenComponents() throws InterruptedException {
161161
}
162162

163163
@Test
164-
public void testChoosesDynamically() throws InterruptedException {
164+
public void testChoosesDynamicallyShouldJumpOverAndStop() throws InterruptedException {
165165
ComponentFlow wizard = ComponentFlow.builder()
166166
.terminal(getTerminal())
167167
.resourceLoader(getResourceLoader())
@@ -202,12 +202,106 @@ public void testChoosesDynamically() throws InterruptedException {
202202
ComponentFlowResult inputWizardResult = result.get();
203203
assertThat(inputWizardResult).isNotNull();
204204
String id1 = inputWizardResult.getContext().get("id1");
205-
// TODO: should be able to check if variable exists
206-
// String id2 = inputWizardResult.getContext().get("id2");
207205
String id3 = inputWizardResult.getContext().get("id3");
208206
assertThat(id1).isEqualTo("id3");
209-
// assertThat(id2).isNull();
210207
assertThat(id3).isEqualTo("value3");
208+
assertThat(inputWizardResult.getContext().containsKey("id2")).isFalse();
209+
}
210+
211+
@Test
212+
public void testChoosesDynamicallyShouldNotContinueToNext() throws InterruptedException {
213+
ComponentFlow wizard = ComponentFlow.builder()
214+
.terminal(getTerminal())
215+
.resourceLoader(getResourceLoader())
216+
.templateExecutor(getTemplateExecutor())
217+
.withStringInput("id1")
218+
.name("name")
219+
.next(ctx -> ctx.get("id1"))
220+
.and()
221+
.withStringInput("id2")
222+
.name("name")
223+
.resultValue("value2")
224+
.resultMode(ResultMode.ACCEPT)
225+
.next(ctx -> null)
226+
.and()
227+
.withStringInput("id3")
228+
.name("name")
229+
.resultValue("value3")
230+
.resultMode(ResultMode.ACCEPT)
231+
.next(ctx -> null)
232+
.and()
233+
.build();
234+
235+
ExecutorService service = Executors.newFixedThreadPool(1);
236+
CountDownLatch latch = new CountDownLatch(1);
237+
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
238+
service.execute(() -> {
239+
result.set(wizard.run());
240+
latch.countDown();
241+
});
242+
243+
// id1
244+
TestBuffer testBuffer = new TestBuffer().append("id2").cr();
245+
write(testBuffer.getBytes());
246+
// id2
247+
testBuffer = new TestBuffer().cr();
248+
write(testBuffer.getBytes());
249+
latch.await(4, TimeUnit.SECONDS);
250+
ComponentFlowResult inputWizardResult = result.get();
251+
assertThat(inputWizardResult).isNotNull();
252+
String id1 = inputWizardResult.getContext().get("id1");
253+
String id2 = inputWizardResult.getContext().get("id2");
254+
assertThat(id1).isEqualTo("id2");
255+
assertThat(id2).isEqualTo("value2");
256+
assertThat(inputWizardResult.getContext().containsKey("id3")).isFalse();
257+
}
258+
259+
@Test
260+
public void testChoosesNonExistingComponent() throws InterruptedException {
261+
ComponentFlow wizard = ComponentFlow.builder()
262+
.terminal(getTerminal())
263+
.resourceLoader(getResourceLoader())
264+
.templateExecutor(getTemplateExecutor())
265+
.withStringInput("id1")
266+
.name("name")
267+
.next(ctx -> ctx.get("id1"))
268+
.and()
269+
.withStringInput("id2")
270+
.name("name")
271+
.resultValue("value2")
272+
.resultMode(ResultMode.ACCEPT)
273+
.next(ctx -> null)
274+
.and()
275+
.withStringInput("id3")
276+
.name("name")
277+
.resultValue("value3")
278+
.resultMode(ResultMode.ACCEPT)
279+
.next(ctx -> null)
280+
.and()
281+
.build();
282+
283+
ExecutorService service = Executors.newFixedThreadPool(1);
284+
CountDownLatch latch = new CountDownLatch(1);
285+
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
286+
service.execute(() -> {
287+
result.set(wizard.run());
288+
latch.countDown();
289+
});
290+
291+
// id1
292+
TestBuffer testBuffer = new TestBuffer().append("fake").cr();
293+
write(testBuffer.getBytes());
294+
295+
// don't execute id2 or id3
296+
testBuffer = new TestBuffer().cr();
297+
write(testBuffer.getBytes());
298+
latch.await(4, TimeUnit.SECONDS);
299+
ComponentFlowResult inputWizardResult = result.get();
300+
assertThat(inputWizardResult).isNotNull();
301+
String id1 = inputWizardResult.getContext().get("id1");
302+
assertThat(id1).isEqualTo("fake");
303+
assertThat(inputWizardResult.getContext().containsKey("id2")).isFalse();
304+
assertThat(inputWizardResult.getContext().containsKey("id3")).isFalse();
211305
}
212306

213307
@Test

spring-shell-docs/src/main/asciidoc/asciinema/component-flow-conditional-1.cast

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121
[12.734384, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K"]
2222
[12.740754, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField1\u001b[0m \u001b[34mdefaultField1Value\u001b[0m\r\n"]
2323
[12.74362, "o", "\u001b[?1h\u001b=\u001b[?25l"]
24-
[12.750944, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField2\u001b[0m \u001b[34m[Default defaultField2Value]\u001b[0m\r"]
25-
[13.542429, "o", "\u001b[?1l\u001b>\u001b[?12;25h\u001b[K"]
26-
[13.545672, "o", "\u001b[32;1m?\u001b[0m \u001b[97;1mField2\u001b[0m \u001b[34mdefaultField2Value\u001b[0m\r\n"]
2724
[13.547149, "o", "\u001b[?1h\u001b=\u001b[?2004h\u001b[33mmy-shell:>\u001b[0m"]
2825
[14.091367, "o", "\u001b[1mflow conditional\u001b[0m"]
2926
[14.419251, "o", "\r\r\n\u001b[?1l\u001b>\u001b[?1000l\u001b[?2004l"]

0 commit comments

Comments
 (0)