From 3c7edd433e64dfffd8319a6bcad1b5e99799ff9b Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sat, 16 Apr 2016 09:50:26 +0100 Subject: [PATCH] Add support for linked choice states - With linked choices a machine got stuck in a pseudostate while it should automatically follow the links till non-pseudostate reached. - Add ChoicePseudoState to use holder as state references may not be known at a time of its instantiation. - Fixes #195 --- .../config/AbstractStateMachineFactory.java | 6 +- .../statemachine/state/ChoicePseudoState.java | 22 +++++-- .../support/AbstractStateMachine.java | 7 +++ .../statemachine/state/ChoiceStateTests.java | 62 ++++++++++++++++++- 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java index 2b401f38..1d458fa2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java @@ -489,7 +489,11 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS List> list = stateMachineTransitions.getChoices().get(s); List> choices = new ArrayList>(); for (ChoiceData c : list) { - choices.add(new ChoiceStateData(stateMap.get(c.getTarget()), c.getGuard())); + StateHolder holder = new StateHolder(stateMap.get(c.getTarget())); + if (holder.getState() == null) { + holderMap.put(c.getTarget(), holder); + } + choices.add(new ChoiceStateData(holder, c.getGuard())); } PseudoState pseudoState = new ChoicePseudoState(choices); state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ChoicePseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ChoicePseudoState.java index 52d0ad46..4c8645c9 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ChoicePseudoState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ChoicePseudoState.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -19,6 +19,7 @@ import java.util.List; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.guard.Guard; +import org.springframework.util.Assert; /** * Choice implementation of a {@link PseudoState}. @@ -74,27 +75,37 @@ public class ChoicePseudoState implements PseudoState { * @param the type of event */ public static class ChoiceStateData { - private final State state; + private final StateHolder state; private final Guard guard; /** * Instantiates a new choice state data. * - * @param state the state + * @param state the state holder * @param guard the guard */ - public ChoiceStateData(State state, Guard guard) { + public ChoiceStateData(StateHolder state, Guard guard) { + Assert.notNull(state, "Holder must be set"); this.state = state; this.guard = guard; } + /** + * Gets the state holder. + * + * @return the state holder + */ + public StateHolder getStateHolder() { + return state; + } + /** * Gets the state. * * @return the state */ public State getState() { - return state; + return state.getState(); } /** @@ -106,5 +117,4 @@ public class ChoicePseudoState implements PseudoState { return guard; } } - } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index 30aa124b..07c5d722 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -729,6 +729,13 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo StateContext stateContext = buildStateContext(Stage.STATE_CHANGED, message, transition, stateMachine); State toState = state.getPseudoState().entry(stateContext); + // TODO: should do recursive linked pseudostates centrally + if (kind == PseudoStateKind.CHOICE) { + while (toState != null && toState.getPseudoState() != null + && toState.getPseudoState().getKind() != PseudoStateKind.INITIAL) { + toState = toState.getPseudoState().entry(stateContext); + } + } if (kind == PseudoStateKind.CHOICE) { callPreStateChangeInterceptors(toState, message, transition, stateMachine); } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/ChoiceStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/ChoiceStateTests.java index 1a8e68f2..41e5e62a 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/ChoiceStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/ChoiceStateTests.java @@ -114,6 +114,20 @@ public class ChoiceStateTests extends AbstractStateMachineTests { assertThat(machine.getState().getIds(), contains(TestStates.S33)); } + @Test + @SuppressWarnings("unchecked") + public void testSubsequentChoiceStates() { + context.register(BaseConfig.class, Config3.class); + context.refresh(); + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("choice", "s2").build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S21)); + } + @Configuration @EnableStateMachine static class Config1 extends EnumStateMachineConfigurerAdapter { @@ -160,6 +174,7 @@ public class ChoiceStateTests extends AbstractStateMachineTests { } } + @Configuration @EnableStateMachine static class Config2 extends EnumStateMachineConfigurerAdapter { @@ -184,9 +199,6 @@ public class ChoiceStateTests extends AbstractStateMachineTests { .and() .withChoice() .source(TestStates.S3) -// .first(TestStates.S30, s30Guard()) -// .then(TestStates.S31, s31Guard()) -// .then(TestStates.S32, s32Guard()) .last(TestStates.S33); } @@ -207,6 +219,50 @@ public class ChoiceStateTests extends AbstractStateMachineTests { } + @Configuration + @EnableStateMachine + static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.SI) + .states(EnumSet.allOf(TestStates.class)) + .choice(TestStates.S3) + .choice(TestStates.S2) + .end(TestStates.SF); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.SI) + .target(TestStates.S3) + .event(TestEvents.E1) + .and() + .withChoice() + .source(TestStates.S3) + .first(TestStates.S2, s2Guard()) + .last(TestStates.S33) + .and() + .withChoice() + .source(TestStates.S2) + .first(TestStates.S20, s20Guard()) + .last(TestStates.S21); + } + + @Bean + public Guard s2Guard() { + return new ChoiceGuard("s2"); + } + + @Bean + public Guard s20Guard() { + return new ChoiceGuard("s20"); + } + } private static class ChoiceGuard implements Guard {