diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/EnumStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/EnumStateMachineFactory.java index 93287ff7..5c064750 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/EnumStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/EnumStateMachineFactory.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Stack; @@ -28,8 +29,10 @@ import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.builders.StateMachineStates; import org.springframework.statemachine.config.builders.StateMachineTransitions; +import org.springframework.statemachine.config.builders.StateMachineTransitions.ChoiceData; import org.springframework.statemachine.config.builders.StateMachineTransitions.TransitionData; import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.state.ChoicePseudoState; import org.springframework.statemachine.state.DefaultPseudoState; import org.springframework.statemachine.state.EnumState; import org.springframework.statemachine.state.PseudoState; @@ -37,6 +40,7 @@ import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.RegionState; import org.springframework.statemachine.state.State; import org.springframework.statemachine.state.StateMachineState; +import org.springframework.statemachine.state.ChoicePseudoState.ChoiceStateData; import org.springframework.statemachine.support.DefaultExtendedState; import org.springframework.statemachine.support.LifecycleObjectSupport; import org.springframework.statemachine.support.tree.Tree; @@ -133,7 +137,7 @@ public class EnumStateMachineFactory, E extends Enum> exten if (initialCount > 1) { for (Collection> regionStateDatas : regionsStateDatas) { machine = buildMachine(machineMap, stateMap, regionStateDatas, transitionsData, getBeanFactory(), - contextEvents, defaultExtendedState); + contextEvents, defaultExtendedState, stateMachineTransitions); if (peek.isInitial() || (!peek.isInitial() && !machineMap.containsKey(peek.getParent()))) { machineMap.put(peek.getParent(), machine); } @@ -161,7 +165,8 @@ public class EnumStateMachineFactory, E extends Enum> exten } else { - machine = buildMachine(machineMap, stateMap, stateDatas, transitionsData, getBeanFactory(), contextEvents, defaultExtendedState); + machine = buildMachine(machineMap, stateMap, stateDatas, transitionsData, getBeanFactory(), + contextEvents, defaultExtendedState, stateMachineTransitions); if (peek.isInitial() || (!peek.isInitial() && !machineMap.containsKey(peek.getParent()))) { machineMap.put(peek.getParent(), machine); } @@ -268,11 +273,16 @@ public class EnumStateMachineFactory, E extends Enum> exten private static , E extends Enum> StateMachine buildMachine( Map> machineMap, Map> stateMap, Collection> stateDatas, Collection> transitionsData, - BeanFactory beanFactory, Boolean contextEvents, DefaultExtendedState defaultExtendedState) { + BeanFactory beanFactory, Boolean contextEvents, DefaultExtendedState defaultExtendedState, + StateMachineTransitions stateMachineTransitions) { State state = null; State initialState = null; Action initialAction = null; Collection> states = new ArrayList>(); + + // for now loop twice and build states for + // non initial/end pseudostates last + for (StateData stateData : stateDatas) { StateMachine stateMachine = machineMap.get(stateData.getState()); if (stateMachine != null) { @@ -289,11 +299,13 @@ public class EnumStateMachineFactory, E extends Enum> exten } states.add(state); } else { - PseudoState pseudoState = null; + PseudoState pseudoState = null; if (stateData.isInitial()) { - pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); } else if (stateData.isEnd()) { - pseudoState = new DefaultPseudoState(PseudoStateKind.END); + pseudoState = new DefaultPseudoState(PseudoStateKind.END); + } else if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) { + continue; } state = new EnumState(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), stateData.getExitActions(), pseudoState); @@ -304,12 +316,25 @@ public class EnumStateMachineFactory, E extends Enum> exten states.add(state); } - - - // TODO: doesn't feel right to tweak initial kind like this stateMap.put(stateData.getState(), state); } + for (StateData stateData : stateDatas) { + if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) { + S s = stateData.getState(); + List> list = stateMachineTransitions.getChoices().get(s); + List> choices = new ArrayList>(); + for (ChoiceData c : list) { + choices.add(new ChoiceStateData(stateMap.get(c.getTarget()), c.getGuard())); + } + PseudoState pseudoState = new ChoicePseudoState(choices); + state = new EnumState(stateData.getState(), stateData.getDeferred(), + stateData.getEntryActions(), stateData.getExitActions(), pseudoState); + states.add(state); + stateMap.put(stateData.getState(), state); + } + } + Collection> transitions = new ArrayList>(); for (TransitionData transitionData : transitionsData) { S source = transitionData.getSource(); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/StateData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/StateData.java index 20a45214..26a069f7 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/StateData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/StateData.java @@ -18,6 +18,7 @@ package org.springframework.statemachine.config; import java.util.Collection; import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.State; /** @@ -41,6 +42,7 @@ public class StateData { private boolean initial = false; private Action initialAction; private boolean end = false; + private PseudoStateKind pseudoStateKind; public StateData(Object parent, Object region, S state, Collection deferred, Collection> entryActions, Collection> exitActions) { @@ -108,11 +110,19 @@ public class StateData { this.end = end; } + public PseudoStateKind getPseudoStateKind() { + return pseudoStateKind; + } + + public void setPseudoStateKind(PseudoStateKind pseudoStateKind) { + this.pseudoStateKind = pseudoStateKind; + } + @Override public String toString() { return "StateData [parent=" + parent + ", region=" + region + ", state=" + state + ", deferred=" + deferred - + ", entryActions=" + entryActions + ", exitActions=" + exitActions + ", initial=" + initial + ", end=" - + end + "]"; + + ", entryActions=" + entryActions + ", exitActions=" + exitActions + ", initial=" + initial + + ", initialAction=" + initialAction + ", end=" + end + ", pseudoStateKind=" + pseudoStateKind + "]"; } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java index dcbd2f82..231dd5db 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionBuilder.java @@ -17,11 +17,18 @@ package org.springframework.statemachine.config.builders; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.config.builders.StateMachineTransitions.ChoiceData; import org.springframework.statemachine.config.builders.StateMachineTransitions.TransitionData; import org.springframework.statemachine.config.common.annotation.AbstractConfiguredAnnotationBuilder; +import org.springframework.statemachine.config.common.annotation.AnnotationBuilder; import org.springframework.statemachine.config.common.annotation.ObjectPostProcessor; +import org.springframework.statemachine.config.configurers.ChoiceTransitionConfigurer; +import org.springframework.statemachine.config.configurers.DefaultChoiceTransitionConfigurer; import org.springframework.statemachine.config.configurers.DefaultExternalTransitionConfigurer; import org.springframework.statemachine.config.configurers.DefaultInternalTransitionConfigurer; import org.springframework.statemachine.config.configurers.DefaultLocalTransitionConfigurer; @@ -31,12 +38,21 @@ import org.springframework.statemachine.config.configurers.LocalTransitionConfig import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.transition.TransitionKind; +/** + * {@link AnnotationBuilder} for {@link StateMachineTransitions}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ public class StateMachineTransitionBuilder extends AbstractConfiguredAnnotationBuilder, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> implements StateMachineTransitionConfigurer { - private Collection> transitionData = new ArrayList>(); + private final Collection> transitionData = new ArrayList>(); + private final Map>> choices = new HashMap>>(); public StateMachineTransitionBuilder() { super(); @@ -53,8 +69,7 @@ public class StateMachineTransitionBuilder @Override protected StateMachineTransitions performBuild() throws Exception { - StateMachineTransitions bean = new StateMachineTransitions(transitionData); - return bean; + return new StateMachineTransitions(transitionData, choices); } @Override @@ -72,8 +87,18 @@ public class StateMachineTransitionBuilder return apply(new DefaultLocalTransitionConfigurer()); } - public void add(S source, S target, S state, E event, Long period, Collection> actions, Guard guard, TransitionKind kind) { + @Override + public ChoiceTransitionConfigurer withChoice() throws Exception { + return apply(new DefaultChoiceTransitionConfigurer()); + } + + public void add(S source, S target, S state, E event, Long period, Collection> actions, + Guard guard, TransitionKind kind) { transitionData.add(new TransitionData(source, target, state, event, period, actions, guard, kind)); } + public void add(S source, List> choices) { + this.choices.put(source, choices); + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionConfigurer.java index 6b4d583d..db79099d 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitionConfigurer.java @@ -15,13 +15,14 @@ */ package org.springframework.statemachine.config.builders; +import org.springframework.statemachine.config.configurers.ChoiceTransitionConfigurer; import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer; import org.springframework.statemachine.config.configurers.InternalTransitionConfigurer; import org.springframework.statemachine.config.configurers.LocalTransitionConfigurer; /** * Configurer interface exposing different type of transitions. - * + * * @author Janne Valkealahti * * @param the type of state @@ -30,8 +31,8 @@ import org.springframework.statemachine.config.configurers.LocalTransitionConfig public interface StateMachineTransitionConfigurer { /** - * Gets a configurer for external transition. - * + * Gets a configurer for external transition. + * * @return {@link ExternalTransitionConfigurer} for chaining * @throws Exception if configuration error happens * @see #withLocal() @@ -44,7 +45,7 @@ public interface StateMachineTransitionConfigurer { * internal transition source and target state is always a same and it is * identical with self-transition in the absence of state entry and exit * actions. - * + * * @return {@link InternalTransitionConfigurer} for chaining * @throws Exception if configuration error happens */ @@ -55,10 +56,18 @@ public interface StateMachineTransitionConfigurer { * exit and entry to source state if target state is a substate of a source * state. Other way around, local transition doesn’t cause exit and entry to * target state if target is a superstate of a source state. - * + * * @return {@link LocalTransitionConfigurer} for chaining * @throws Exception if configuration error happens */ LocalTransitionConfigurer withLocal() throws Exception; - + + /** + * Gets a configurer for transition from a choice pseudostate. + * + * @return {@link LocalTransitionConfigurer} for chaining + * @throws Exception if configuration error happens + */ + ChoiceTransitionConfigurer withChoice() throws Exception; + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitions.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitions.java index 7b45ea80..3b743812 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitions.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineTransitions.java @@ -16,23 +16,39 @@ package org.springframework.statemachine.config.builders; import java.util.Collection; +import java.util.List; +import java.util.Map; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.transition.TransitionKind; +/** + * Data object for transitions. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ public class StateMachineTransitions { private Collection> transitions; + private Map>> choices; - public StateMachineTransitions(Collection> transitions) { + public StateMachineTransitions(Collection> transitions, Map>> choices) { this.transitions = transitions; + this.choices = choices; } public Collection> getTransitions() { return transitions; } + public Map>> getChoices() { + return choices; + } + public static class TransitionData { S source; S target; @@ -78,4 +94,25 @@ public class StateMachineTransitions { } } + public static class ChoiceData { + private final S source; + private final S target; + private final Guard guard; + public ChoiceData(S source, S target, Guard guard) { + this.source = source; + this.target = target; + this.guard = guard; + } + public S getSource() { + return source; + } + public S getTarget() { + return target; + } + public Guard getGuard() { + return guard; + } + } + + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ChoiceTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ChoiceTransitionConfigurer.java new file mode 100644 index 00000000..9b5ea47a --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ChoiceTransitionConfigurer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 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 + * + * http://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.statemachine.config.configurers; + +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder; +import org.springframework.statemachine.guard.Guard; +import org.springframework.statemachine.transition.Transition; + +/** + * {@code TransitionConfigurer} interface for configuring {@link Transition} + * from a choice pseudo state. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface ChoiceTransitionConfigurer + extends AnnotationConfigurerBuilder> { + + /** + * Specify a source state {@code S} for this {@link Transition}. + * + * @param source the source state {@code S} + * @return configurer for chaining + */ + ChoiceTransitionConfigurer source(S source); + + /** + * Specify a target state {@code S} as a first choice. + * This must be set. + *

In normal if/else if/else this would represent if.

+ * + * @param target the target state + * @param guard the guard for this choice + * @return configurer for chaining + */ + ChoiceTransitionConfigurer first(S target, Guard guard); + + /** + * Specify a target state {@code S} as a then choice. + * This is optional. Multiple thens will preserve order. + *

In normal if/else if/else this would represent else if.

+ * + * @param target the target state + * @param guard the guard for this choice + * @return configurer for chaining + */ + ChoiceTransitionConfigurer then(S target, Guard guard); + + /** + * Specify a target state {@code S} as a last choice. + * This must be set. + *

In normal if/else if/else this would represent else.

+ * + * @param target the target state + * @return configurer for chaining + */ + ChoiceTransitionConfigurer last(S target); + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultChoiceTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultChoiceTransitionConfigurer.java new file mode 100644 index 00000000..7b187812 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultChoiceTransitionConfigurer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 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 + * + * http://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.statemachine.config.configurers; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitions; +import org.springframework.statemachine.config.builders.StateMachineTransitions.ChoiceData; +import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter; +import org.springframework.statemachine.guard.Guard; + +/** + * Default implementation of a {@link ChoiceTransitionConfigurer}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class DefaultChoiceTransitionConfigurer + extends AnnotationConfigurerAdapter, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> + implements ChoiceTransitionConfigurer { + + private S source; + + private ChoiceData first; + + private final List> thens = new ArrayList>(); + + private ChoiceData last; + + @Override + public void configure(StateMachineTransitionBuilder builder) throws Exception { + List> choices = new ArrayList>(); + choices.add(first); + choices.addAll(thens); + choices.add(last); + builder.add(source, choices); + } + + @Override + public ChoiceTransitionConfigurer source(S source) { + this.source = source; + return this; + } + + @Override + public ChoiceTransitionConfigurer first(S target, Guard guard) { + this.first = new ChoiceData(source, target, guard); + return this; + } + + @Override + public ChoiceTransitionConfigurer then(S target, Guard guard) { + thens.add(new ChoiceData(source, target, guard)); + return this; + } + + @Override + public ChoiceTransitionConfigurer last(S target) { + this.last = new ChoiceData(source, target, null); + return this; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java index 047685b7..2551fba0 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java @@ -27,6 +27,7 @@ import org.springframework.statemachine.config.builders.StateMachineStateBuilder import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineStates; import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter; +import org.springframework.statemachine.state.PseudoStateKind; /** * Default implementation of a {@link StateConfigurer}. @@ -52,6 +53,8 @@ public class DefaultStateConfigurer private S end; + private final Collection choices = new ArrayList(); + @Override public void configure(StateMachineStateBuilder builder) throws Exception { // before passing state datas to builder, update structure @@ -67,6 +70,9 @@ public class DefaultStateConfigurer if (s.getState() == end) { s.setEnd(true); } + if (choices.contains(s.getState())) { + s.setPseudoStateKind(PseudoStateKind.CHOICE); + } } builder.addStateData(stateDatas); } @@ -140,6 +146,12 @@ public class DefaultStateConfigurer return this; } + @Override + public StateConfigurer choice(S choice) { + choices.add(choice); + return this; + } + private void addIncomplete(Object parent, S state, Collection deferred, Collection> entryActions, Collection> exitActions) { incomplete.add(new StateData(parent, region, state, deferred, entryActions, exitActions)); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java index 64c18db4..e16b4cff 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java @@ -115,4 +115,12 @@ public interface StateConfigurer extends */ StateConfigurer end(S end); + /** + * Specify a state {@code S} to be choice pseudo state. + * + * @param choice the choice pseudo state + * @return configurer for chaining + */ + StateConfigurer choice(S choice); + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractPseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractPseudoState.java index 8d03b93a..1c6e755c 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractPseudoState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractPseudoState.java @@ -15,13 +15,17 @@ */ package org.springframework.statemachine.state; +import org.springframework.statemachine.StateContext; + /** * Base implementation of a {@link PseudoState}. - * + * * @author Janne Valkealahti * + * @param the type of state + * @param the type of event */ -public abstract class AbstractPseudoState implements PseudoState { +public abstract class AbstractPseudoState implements PseudoState { private final PseudoStateKind kind; @@ -39,4 +43,9 @@ public abstract class AbstractPseudoState implements PseudoState { return kind; } + @Override + public State entry(E event, StateContext context) { + return null; + } + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java index f52feb76..32a97578 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractSimpleState.java @@ -73,7 +73,7 @@ public abstract class AbstractSimpleState extends AbstractState { * @param id the state identifier * @param pseudoState the pseudo state */ - public AbstractSimpleState(S id, PseudoState pseudoState) { + public AbstractSimpleState(S id, PseudoState pseudoState) { this(id, null, null, null, pseudoState); } @@ -88,7 +88,7 @@ public abstract class AbstractSimpleState extends AbstractState { * @param regions the regions */ public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, - Collection> exitActions, PseudoState pseudoState, Collection> regions) { + Collection> exitActions, PseudoState pseudoState, Collection> regions) { super(id, deferred, entryActions, exitActions, pseudoState, regions); this.ids = new ArrayList(); this.ids.add(id); @@ -105,7 +105,7 @@ public abstract class AbstractSimpleState extends AbstractState { * @param submachine the submachine */ public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, - Collection> exitActions, PseudoState pseudoState, StateMachine submachine) { + Collection> exitActions, PseudoState pseudoState, StateMachine submachine) { super(id, deferred, entryActions, exitActions, pseudoState, submachine); this.ids = new ArrayList(); this.ids.add(id); @@ -121,7 +121,7 @@ public abstract class AbstractSimpleState extends AbstractState { * @param pseudoState the pseudo state */ public AbstractSimpleState(S id, Collection deferred, Collection> entryActions, - Collection> exitActions, PseudoState pseudoState) { + Collection> exitActions, PseudoState pseudoState) { super(id, deferred, entryActions, exitActions, pseudoState); this.ids = new ArrayList(); this.ids.add(id); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java index 62bb99a7..80a8a1de 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java @@ -35,7 +35,7 @@ import org.springframework.statemachine.region.Region; public abstract class AbstractState implements State { private final S id; - private final PseudoState pseudoState; + private final PseudoState pseudoState; private final Collection deferred; private final Collection> entryActions; private final Collection> exitActions; @@ -48,7 +48,7 @@ public abstract class AbstractState implements State { * @param id the state identifier * @param pseudoState the pseudo state */ - public AbstractState(S id, PseudoState pseudoState) { + public AbstractState(S id, PseudoState pseudoState) { this(id, null, null, null, pseudoState); } @@ -85,7 +85,7 @@ public abstract class AbstractState implements State { * @param pseudoState the pseudo state */ public AbstractState(S id, Collection deferred, Collection> entryActions, - Collection> exitActions, PseudoState pseudoState) { + Collection> exitActions, PseudoState pseudoState) { this(id, deferred, entryActions, exitActions, pseudoState, null, null); } @@ -100,7 +100,7 @@ public abstract class AbstractState implements State { * @param submachine the submachine */ public AbstractState(S id, Collection deferred, Collection> entryActions, - Collection> exitActions, PseudoState pseudoState, StateMachine submachine) { + Collection> exitActions, PseudoState pseudoState, StateMachine submachine) { this(id, deferred, entryActions, exitActions, pseudoState, null, submachine); } @@ -115,7 +115,7 @@ public abstract class AbstractState implements State { * @param regions the regions */ public AbstractState(S id, Collection deferred, Collection> entryActions, - Collection> exitActions, PseudoState pseudoState, Collection> regions) { + Collection> exitActions, PseudoState pseudoState, Collection> regions) { this(id, deferred, entryActions, exitActions, pseudoState, regions, null); } @@ -131,7 +131,7 @@ public abstract class AbstractState implements State { * @param submachine the submachine */ private AbstractState(S id, Collection deferred, Collection> entryActions, - Collection> exitActions, PseudoState pseudoState, Collection> regions, + Collection> exitActions, PseudoState pseudoState, Collection> regions, StateMachine submachine) { this.id = id; this.deferred = deferred; @@ -170,7 +170,7 @@ public abstract class AbstractState implements State { public abstract Collection> getStates(); @Override - public PseudoState getPseudoState() { + public PseudoState getPseudoState() { return pseudoState; } 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 new file mode 100644 index 00000000..245379da --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ChoicePseudoState.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 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 + * + * http://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.statemachine.state; + +import java.util.List; + +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.guard.Guard; + +/** + * Choice implementation of a {@link PseudoState}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class ChoicePseudoState implements PseudoState { + + private List> choices; + + public ChoicePseudoState(List> choices) { + this.choices = choices; + } + + @Override + public PseudoStateKind getKind() { + return PseudoStateKind.CHOICE; + } + + @Override + public State entry(E event, StateContext context) { + State s = null; + for (ChoiceStateData c : choices) { + s = c.getState(); + if (c.guard != null && c.guard.evaluate(context)) { + break; + } + } + return s; + } + + public static class ChoiceStateData { + private final State state; + private final Guard guard; + public ChoiceStateData(State state, Guard guard) { + this.state = state; + this.guard = guard; + } + public State getState() { + return state; + } + public Guard getGuard() { + return guard; + } + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/DefaultPseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/DefaultPseudoState.java index 26cd595f..a6551457 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/DefaultPseudoState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/DefaultPseudoState.java @@ -18,11 +18,13 @@ package org.springframework.statemachine.state; /** * Default implementation of a {@link PseudoState} which is a simple passthrough * via {@link AbstractPseudoState}. - * + * * @author Janne Valkealahti * + * @param the type of state + * @param the type of event */ -public class DefaultPseudoState extends AbstractPseudoState { +public class DefaultPseudoState extends AbstractPseudoState { /** * Instantiates a new default pseudo state. diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/EnumState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/EnumState.java index 283b4539..e7aea2e6 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/EnumState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/EnumState.java @@ -47,7 +47,7 @@ public class EnumState, E extends Enum> extends AbstractSim * @param id the id * @param pseudoState the pseudo state */ - public EnumState(S id, PseudoState pseudoState) { + public EnumState(S id, PseudoState pseudoState) { super(id, pseudoState); } @@ -83,7 +83,7 @@ public class EnumState, E extends Enum> extends AbstractSim * @param pseudoState the pseudo state */ public EnumState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState) { + PseudoState pseudoState) { super(id, deferred, entryActions, exitActions, pseudoState); } @@ -98,7 +98,7 @@ public class EnumState, E extends Enum> extends AbstractSim * @param regions the regions */ public EnumState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState, Collection> regions) { + PseudoState pseudoState, Collection> regions) { super(id, deferred, entryActions, exitActions, pseudoState, regions); } @@ -113,7 +113,7 @@ public class EnumState, E extends Enum> extends AbstractSim * @param submachine the submachine */ public EnumState(S id, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState, StateMachine submachine) { + PseudoState pseudoState, StateMachine submachine) { super(id, deferred, entryActions, exitActions, pseudoState, submachine); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoState.java index 61452493..d93866dc 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoState.java @@ -15,21 +15,25 @@ */ package org.springframework.statemachine.state; +import org.springframework.statemachine.StateContext; + /** * A {@code PseudoState} is an abstraction that encompasses different types of * transient states or vertices in the state machine. - * + * *

* Pseudostates are typically used to connect multiple transitions into more * complex state transitions paths. For example, by combining a transition * entering a fork pseudostate with a set of transitions exiting the fork * pseudostate, we get a compound transition that leads to a set of orthogonal * target states. - * + * * @author Janne Valkealahti * + * @param the type of state + * @param the type of event */ -public interface PseudoState { +public interface PseudoState { /** * Gets the pseudostate kind. @@ -38,4 +42,14 @@ public interface PseudoState { */ PseudoStateKind getKind(); + /** + * Initiate an entry sequence for the state and return a next + * state where state machine should go. + * + * @param event the event + * @param context the context + * @return the next state or null + */ + State entry(E event, StateContext context); + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateKind.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateKind.java index 14d458e7..d53d496e 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateKind.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateKind.java @@ -28,6 +28,9 @@ public enum PseudoStateKind { INITIAL, /** End or terminate kind */ - END + END, + + /** Choice kind */ + CHOICE } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java index 02b26edd..721178d3 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java @@ -61,7 +61,7 @@ public class RegionState extends AbstractState { * @param regions the regions * @param pseudoState the pseudo state */ - public RegionState(S id, Collection> regions, PseudoState pseudoState) { + public RegionState(S id, Collection> regions, PseudoState pseudoState) { super(id, null, null, null, pseudoState, regions); } @@ -76,7 +76,7 @@ public class RegionState extends AbstractState { * @param pseudoState the pseudo state */ public RegionState(S id, Collection> regions, Collection deferred, - Collection> entryActions, Collection> exitActions, PseudoState pseudoState) { + Collection> entryActions, Collection> exitActions, PseudoState pseudoState) { super(id, deferred, entryActions, exitActions, pseudoState, regions); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java index ca1b4db0..fe0a50c5 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java @@ -85,7 +85,7 @@ public interface State { * * @return pseudostate or null if state doesn't have one */ - PseudoState getPseudoState(); + PseudoState getPseudoState(); /** * Gets the deferred events for this state. diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java index 99c11034..f030d3e8 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java @@ -69,7 +69,7 @@ public class StateMachineState extends AbstractState { * @param submachine the submachine * @param pseudoState the pseudo state */ - public StateMachineState(S id, StateMachine submachine, PseudoState pseudoState) { + public StateMachineState(S id, StateMachine submachine, PseudoState pseudoState) { super(id, null, null, null, pseudoState, submachine); this.ids = new ArrayList(); this.ids.add(id); @@ -87,7 +87,7 @@ public class StateMachineState extends AbstractState { */ public StateMachineState(S id, StateMachine submachine, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState) { + PseudoState pseudoState) { super(id, deferred, entryActions, exitActions, pseudoState, submachine); this.ids = new ArrayList(); this.ids.add(id); 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 d3f51632..4c1893fb 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 @@ -319,7 +319,18 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport } private void switchToState(State state, Message event, Transition transition, StateMachine stateMachine) { - setCurrentState(state, event, transition, true, stateMachine); + // TODO: need to make below more clear when + // we figure out rest of a pseudostates + if (state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.CHOICE) { + MessageHeaders messageHeaders = event != null ? event.getHeaders() : new MessageHeaders( + new HashMap()); + StateContext stateContext = new DefaultStateContext(event != null ? event.getPayload() : null, + messageHeaders, extendedState, transition, stateMachine); + State entry = state.getPseudoState().entry(event.getPayload(), stateContext); + setCurrentState(entry, event, transition, true, stateMachine); + } else { + setCurrentState(state, event, transition, true, stateMachine); + } // TODO: should handle triggerles transition some how differently for (Transition t : transitions) { @@ -343,7 +354,7 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport return null; } - void setCurrentState(State state, Message event, Transition transition, boolean exit, StateMachine stateMachine) { + public void setCurrentState(State state, Message event, Transition transition, boolean exit, StateMachine stateMachine) { State findDeep = findDeepParent(state); boolean isTargetSubOf = false; if (transition != null) { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java index 1aa303e0..7ab88359 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java @@ -66,7 +66,8 @@ public abstract class AbstractStateMachineTests { SI,S1,S2,S3,S4,SF, S10,S11,S101,S111,S112,S12,S121,S122,S13, S20,S21,S201,S211, - S1011,S1012,S2011,S2012 + S1011,S1012,S2011,S2012, + S30,S31,S32,S33 } public enum TestEvents { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java index 88e7d1a7..a8b5ce99 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java @@ -41,7 +41,7 @@ public class EnumStateMachineTests extends AbstractStateMachineTests { @Test public void testSimpleStateSwitch() { - PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); State stateSI = new EnumState(TestStates.SI, pseudoState); State stateS1 = new EnumState(TestStates.S1); State stateS2 = new EnumState(TestStates.S2); @@ -106,7 +106,7 @@ public class EnumStateMachineTests extends AbstractStateMachineTests { @Test public void testDeferredEvents() { - PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); Collection deferred = new ArrayList(); deferred.add(TestEvents.E2); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java index 0772c63b..78685c76 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java @@ -65,7 +65,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { @Test public void testSimpleRegionBuildRaw() throws Exception { - PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); TestEntryAction entryActionS1 = new TestEntryAction("S1"); TestExitAction exitActionS1 = new TestExitAction("S1"); Collection> entryActionsS1 = new ArrayList>(); @@ -129,7 +129,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { @Test public void testMultiRegionBuildRaw() throws Exception { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); - PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); State stateSI = new EnumState(TestStates.SI); TestEntryAction entryActionS111 = new TestEntryAction("S111"); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java index 42eb7b59..3e6ff7bc 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java @@ -80,7 +80,7 @@ public class SubStateMachineTests extends AbstractStateMachineTests { * | | * +-------------------------------------------+ */ - PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); TestEntryAction entryActionS111 = new TestEntryAction("S111"); TestExitAction exitActionS111 = new TestExitAction("S111"); @@ -175,7 +175,7 @@ public class SubStateMachineTests extends AbstractStateMachineTests { */ - PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); TestEntryAction entryActionS111 = new TestEntryAction("S111"); TestExitAction exitActionS111 = new TestExitAction("S111"); @@ -267,7 +267,7 @@ public class SubStateMachineTests extends AbstractStateMachineTests { * | | * +-------------------------------------------+ */ - PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); TestEntryAction entryActionS111 = new TestEntryAction("S111"); TestExitAction exitActionS111 = new TestExitAction("S111"); 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 new file mode 100644 index 00000000..e1eed48f --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/ChoiceStateTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2015 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 + * + * http://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.statemachine.state; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.util.EnumSet; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.EnumStateMachine; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.guard.Guard; +import org.springframework.util.ObjectUtils; + +public class ChoiceStateTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + @SuppressWarnings("unchecked") + public void testFirst() { + context.register(BaseConfig.class, Config1.class); + context.refresh(); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("choice", "s30").build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S30)); + } + + @Test + @SuppressWarnings("unchecked") + public void testThen1() { + context.register(BaseConfig.class, Config1.class); + context.refresh(); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("choice", "s31").build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S31)); + } + + @Test + @SuppressWarnings("unchecked") + public void testThen2() { + context.register(BaseConfig.class, Config1.class); + context.refresh(); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("choice", "s32").build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S32)); + } + + @Test + @SuppressWarnings("unchecked") + public void testLast() { + context.register(BaseConfig.class, Config1.class); + context.refresh(); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S33)); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.SI) + .states(EnumSet.allOf(TestStates.class)) + .choice(TestStates.S3) + .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.S30, s30Guard()) + .then(TestStates.S31, s31Guard()) + .then(TestStates.S32, s32Guard()) + .last(TestStates.S33); + } + + @Bean + public Guard s30Guard() { + return new ChoiceGuard("s30"); + } + + @Bean + public Guard s31Guard() { + return new ChoiceGuard("s31"); + } + + @Bean + public Guard s32Guard() { + return new ChoiceGuard("s32"); + } + + } + + private static class ChoiceGuard implements Guard { + + private final String match; + + public ChoiceGuard(String match) { + this.match = match; + } + + @Override + public boolean evaluate(StateContext context) { + return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("choice", String.class)); + } + } + +}