From 49b158d3c69db94bce90ccf8457b6ff83d48d216 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sat, 16 Apr 2016 16:46:00 +0100 Subject: [PATCH] Add support for junction pseudostate - Pretty much as choice as difference is more or less academic but needed for uml model. - Fixes #42 --- .../config/AbstractStateMachineFactory.java | 21 ++ .../StateMachineTransitionBuilder.java | 23 +- .../StateMachineTransitionConfigurer.java | 9 + .../DefaultJunctionTransitionConfigurer.java | 82 +++++ .../configurers/DefaultStateConfigurer.java | 10 + .../JunctionTransitionConfigurer.java | 75 +++++ .../config/configurers/StateConfigurer.java | 8 + .../config/model/JunctionData.java | 70 +++++ .../config/model/TransitionsData.java | 19 +- .../state/JunctionPseudoState.java | 120 ++++++++ .../statemachine/state/PseudoStateKind.java | 3 + .../support/AbstractStateMachine.java | 12 +- .../config/model/StateMachineModelTests.java | 3 +- .../state/JunctionStateTests.java | 281 ++++++++++++++++++ .../statemachine/uml/UmlModelParser.java | 2 +- 15 files changed, 730 insertions(+), 8 deletions(-) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultJunctionTransitionConfigurer.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/JunctionTransitionConfigurer.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/JunctionData.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JunctionPseudoState.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JunctionStateTests.java 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 1d458fa2..b4bc7003 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 @@ -40,6 +40,7 @@ import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.model.ChoiceData; import org.springframework.statemachine.config.model.EntryData; import org.springframework.statemachine.config.model.ExitData; +import org.springframework.statemachine.config.model.JunctionData; import org.springframework.statemachine.config.model.StateData; import org.springframework.statemachine.config.model.StateMachineModel; import org.springframework.statemachine.config.model.TransitionData; @@ -59,6 +60,8 @@ import org.springframework.statemachine.state.ExitPseudoState; import org.springframework.statemachine.state.ForkPseudoState; import org.springframework.statemachine.state.HistoryPseudoState; import org.springframework.statemachine.state.JoinPseudoState; +import org.springframework.statemachine.state.JunctionPseudoState; +import org.springframework.statemachine.state.JunctionPseudoState.JunctionStateData; import org.springframework.statemachine.state.PseudoState; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.RegionState; @@ -466,6 +469,8 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS continue; } else if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) { continue; + } else if (stateData.getPseudoStateKind() == PseudoStateKind.JUNCTION) { + continue; } else if (stateData.getPseudoStateKind() == PseudoStateKind.ENTRY) { continue; } else if (stateData.getPseudoStateKind() == PseudoStateKind.EXIT) { @@ -500,6 +505,22 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS stateData.getExitActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); + } else if (stateData.getPseudoStateKind() == PseudoStateKind.JUNCTION) { + S s = stateData.getState(); + List> list = stateMachineTransitions.getJunctions().get(s); + List> junctions = new ArrayList>(); + for (JunctionData c : list) { + StateHolder holder = new StateHolder(stateMap.get(c.getTarget())); + if (holder.getState() == null) { + holderMap.put(c.getTarget(), holder); + } + junctions.add(new JunctionStateData(holder, c.getGuard())); + } + PseudoState pseudoState = new JunctionPseudoState(junctions); + state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(), + stateData.getExitActions(), pseudoState); + states.add(state); + stateMap.put(stateData.getState(), state); } else if (stateData.getPseudoStateKind() == PseudoStateKind.ENTRY) { S s = stateData.getState(); Collection> entrys = stateMachineTransitions.getEntrys(); 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 79654676..a6e739e1 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 @@ -33,6 +33,7 @@ import org.springframework.statemachine.config.configurers.DefaultExternalTransi import org.springframework.statemachine.config.configurers.DefaultForkTransitionConfigurer; import org.springframework.statemachine.config.configurers.DefaultInternalTransitionConfigurer; import org.springframework.statemachine.config.configurers.DefaultJoinTransitionConfigurer; +import org.springframework.statemachine.config.configurers.DefaultJunctionTransitionConfigurer; import org.springframework.statemachine.config.configurers.DefaultLocalTransitionConfigurer; import org.springframework.statemachine.config.configurers.EntryTransitionConfigurer; import org.springframework.statemachine.config.configurers.ExitTransitionConfigurer; @@ -40,13 +41,15 @@ import org.springframework.statemachine.config.configurers.ExternalTransitionCon import org.springframework.statemachine.config.configurers.ForkTransitionConfigurer; import org.springframework.statemachine.config.configurers.InternalTransitionConfigurer; import org.springframework.statemachine.config.configurers.JoinTransitionConfigurer; +import org.springframework.statemachine.config.configurers.JunctionTransitionConfigurer; import org.springframework.statemachine.config.configurers.LocalTransitionConfigurer; import org.springframework.statemachine.config.model.ChoiceData; import org.springframework.statemachine.config.model.ConfigurationData; import org.springframework.statemachine.config.model.EntryData; import org.springframework.statemachine.config.model.ExitData; -import org.springframework.statemachine.config.model.TransitionsData; +import org.springframework.statemachine.config.model.JunctionData; import org.springframework.statemachine.config.model.TransitionData; +import org.springframework.statemachine.config.model.TransitionsData; import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.security.SecurityRule; import org.springframework.statemachine.transition.TransitionKind; @@ -66,6 +69,7 @@ public class StateMachineTransitionBuilder private final Collection> transitionData = new ArrayList>(); private final Map>> choices = new HashMap>>(); + private final Map>> junctions = new HashMap>>(); private final Map> forks = new HashMap>(); private final Map> joins = new HashMap>(); private final Collection> entryData = new ArrayList>(); @@ -100,7 +104,7 @@ public class StateMachineTransitionBuilder @Override protected TransitionsData performBuild() throws Exception { - return new TransitionsData(transitionData, choices, forks, joins, entryData, exitData); + return new TransitionsData(transitionData, choices, junctions, forks, joins, entryData, exitData); } @Override @@ -123,6 +127,11 @@ public class StateMachineTransitionBuilder return apply(new DefaultChoiceTransitionConfigurer()); } + @Override + public JunctionTransitionConfigurer withJunction() throws Exception { + return apply(new DefaultJunctionTransitionConfigurer()); + } + @Override public ForkTransitionConfigurer withFork() throws Exception { return apply(new DefaultForkTransitionConfigurer()); @@ -178,6 +187,16 @@ public class StateMachineTransitionBuilder this.choices.put(source, choices); } + /** + * Adds the junction. + * + * @param source the source + * @param junctions the junctions + */ + public void addJunction(S source, List> junctions) { + this.junctions.put(source, junctions); + } + /** * Adds the entry. * 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 63e0adfd..dadd6083 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 @@ -22,6 +22,7 @@ import org.springframework.statemachine.config.configurers.ExternalTransitionCon import org.springframework.statemachine.config.configurers.ForkTransitionConfigurer; import org.springframework.statemachine.config.configurers.InternalTransitionConfigurer; import org.springframework.statemachine.config.configurers.JoinTransitionConfigurer; +import org.springframework.statemachine.config.configurers.JunctionTransitionConfigurer; import org.springframework.statemachine.config.configurers.LocalTransitionConfigurer; /** @@ -74,6 +75,14 @@ public interface StateMachineTransitionConfigurer { */ ChoiceTransitionConfigurer withChoice() throws Exception; + /** + * Gets a configurer for transition from a junction pseudostate. + * + * @return {@link JunctionTransitionConfigurer} for chaining + * @throws Exception if configuration error happens + */ + JunctionTransitionConfigurer withJunction() throws Exception; + /** * Gets a configurer for transition from a fork pseudostate. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultJunctionTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultJunctionTransitionConfigurer.java new file mode 100644 index 00000000..ada1a505 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultJunctionTransitionConfigurer.java @@ -0,0 +1,82 @@ +/* + * Copyright 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. + * 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.common.annotation.AnnotationConfigurerAdapter; +import org.springframework.statemachine.config.model.JunctionData; +import org.springframework.statemachine.config.model.TransitionsData; +import org.springframework.statemachine.guard.Guard; + +/** + * Default implementation of a {@link JunctionTransitionConfigurer}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class DefaultJunctionTransitionConfigurer + extends AnnotationConfigurerAdapter, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> + implements JunctionTransitionConfigurer { + + private S source; + private JunctionData first; + private final List> thens = new ArrayList>(); + private JunctionData last; + + @Override + public void configure(StateMachineTransitionBuilder builder) throws Exception { + List> Junctions = new ArrayList>(); + if (first != null) { + Junctions.add(first); + } + Junctions.addAll(thens); + if (last != null) { + Junctions.add(last); + } + builder.addJunction(source, Junctions); + } + + @Override + public JunctionTransitionConfigurer source(S source) { + this.source = source; + return this; + } + + @Override + public JunctionTransitionConfigurer first(S target, Guard guard) { + this.first = new JunctionData(source, target, guard); + return this; + } + + @Override + public JunctionTransitionConfigurer then(S target, Guard guard) { + thens.add(new JunctionData(source, target, guard)); + return this; + } + + @Override + public JunctionTransitionConfigurer last(S target) { + this.last = new JunctionData(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 410d1e5a..6267b03f 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 @@ -52,6 +52,7 @@ public class DefaultStateConfigurer private S history; private History historyType; private final Collection choices = new ArrayList(); + private final Collection junctions = new ArrayList(); private final Collection forks = new ArrayList(); private final Collection joins = new ArrayList(); private final Collection exits = new ArrayList(); @@ -74,6 +75,8 @@ public class DefaultStateConfigurer } if (choices.contains(s.getState())) { s.setPseudoStateKind(PseudoStateKind.CHOICE); + } else if (junctions.contains(s.getState())) { + s.setPseudoStateKind(PseudoStateKind.JUNCTION); } else if (forks.contains(s.getState())) { s.setPseudoStateKind(PseudoStateKind.FORK); } else if (joins.contains(s.getState())) { @@ -173,6 +176,13 @@ public class DefaultStateConfigurer return this; } + @Override + public StateConfigurer junction(S junction) { + state(junction); + junctions.add(junction); + return this; + } + @Override public StateConfigurer fork(S fork) { state(fork); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/JunctionTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/JunctionTransitionConfigurer.java new file mode 100644 index 00000000..4abe869d --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/JunctionTransitionConfigurer.java @@ -0,0 +1,75 @@ +/* + * Copyright 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. + * 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 junction pseudo state. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface JunctionTransitionConfigurer + extends AnnotationConfigurerBuilder> { + + /** + * Specify a source state {@code S} for this {@link Transition}. + * + * @param source the source state {@code S} + * @return configurer for chaining + */ + JunctionTransitionConfigurer 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 + */ + JunctionTransitionConfigurer 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 + */ + JunctionTransitionConfigurer 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 + */ + JunctionTransitionConfigurer last(S target); + +} 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 62424a19..5d1b57fd 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 @@ -124,6 +124,14 @@ public interface StateConfigurer extends */ StateConfigurer choice(S choice); + /** + * Specify a state {@code S} to be junction pseudo state. + * + * @param junction the junction pseudo state + * @return configurer for chaining + */ + StateConfigurer junction(S junction); + /** * Specify a state {@code S} to be fork pseudo state. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/JunctionData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/JunctionData.java new file mode 100644 index 00000000..4bfdcda1 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/JunctionData.java @@ -0,0 +1,70 @@ +/* + * Copyright 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. + * 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.model; + +import org.springframework.statemachine.guard.Guard; + +/** + * A simple data object keeping junction related configs in a same place. + * + * @param the type of state + * @param the type of event + */ +public class JunctionData { + private final S source; + private final S target; + private final Guard guard; + + /** + * Instantiates a new junction data. + * + * @param source the source + * @param target the target + * @param guard the guard + */ + public JunctionData(S source, S target, Guard guard) { + this.source = source; + this.target = target; + this.guard = guard; + } + + /** + * Gets the source. + * + * @return the source + */ + public S getSource() { + return source; + } + + /** + * Gets the target. + * + * @return the target + */ + public S getTarget() { + return target; + } + + /** + * Gets the guard. + * + * @return the guard + */ + public Guard getGuard() { + return guard; + } +} \ No newline at end of file diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/TransitionsData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/TransitionsData.java index f28ef153..78b665c0 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/TransitionsData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/TransitionsData.java @@ -31,6 +31,7 @@ public class TransitionsData { private final Collection> transitions; private final Map>> choices; + private final Map>> junctions; private final Map> forks; private final Map> joins; private final Collection> entrys; @@ -42,7 +43,7 @@ public class TransitionsData { * @param transitionsData the transitions data */ public TransitionsData(Collection> transitionsData) { - this(transitionsData, null, null, null, null, null); + this(transitionsData, null, null, null, null, null, null); } /** @@ -50,15 +51,18 @@ public class TransitionsData { * * @param transitionsData the transitions data * @param choices the choices + * @param junctions the junctions * @param forks the forks * @param joins the joins * @param entrys the entrys * @param exits the exits */ - public TransitionsData(Collection> transitionsData, Map>> choices, Map> forks, - Map> joins, Collection> entrys, Collection> exits) { + public TransitionsData(Collection> transitionsData, Map>> choices, + Map>> junctions, Map> forks, Map> joins, Collection> entrys, + Collection> exits) { this.transitions = transitionsData; this.choices = choices; + this.junctions = junctions; this.forks = forks; this.joins = joins; this.entrys = entrys; @@ -83,6 +87,15 @@ public class TransitionsData { return choices; } + /** + * Gets the junctions. + * + * @return the junctions + */ + public Map>> getJunctions() { + return junctions; + } + /** * Gets the forks. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JunctionPseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JunctionPseudoState.java new file mode 100644 index 00000000..dada89e1 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JunctionPseudoState.java @@ -0,0 +1,120 @@ +/* + * Copyright 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. + * 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; +import org.springframework.util.Assert; + +/** + * Junction implementation of a {@link PseudoState}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class JunctionPseudoState implements PseudoState { + + private final List> junctions; + + /** + * Instantiates a new junction pseudo state. + * + * @param junctions the junctions + */ + public JunctionPseudoState(List> junctions) { + this.junctions = junctions; + } + + @Override + public PseudoStateKind getKind() { + return PseudoStateKind.JUNCTION; + } + + @Override + public State entry(StateContext context) { + State s = null; + for (JunctionStateData j : junctions) { + s = j.getState(); + if (j.guard != null && j.guard.evaluate(context)) { + break; + } + } + return s; + } + + @Override + public void exit(StateContext context) { + } + + @Override + public void addPseudoStateListener(PseudoStateListener listener) { + } + + /** + * Data class wrapping choice {@link State} and {@link Guard} + * together. + * + * @param the type of state + * @param the type of event + */ + public static class JunctionStateData { + private final StateHolder state; + private final Guard guard; + + /** + * Instantiates a new junction state data. + * + * @param state the state holder + * @param guard the guard + */ + public JunctionStateData(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.getState(); + } + + /** + * Gets the guard. + * + * @return the guard + */ + public Guard getGuard() { + return guard; + } + } +} 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 c2f5ff4c..6cfb967e 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 @@ -33,6 +33,9 @@ public enum PseudoStateKind { /** Choice kind */ CHOICE, + /** Junction kind */ + JUNCTION, + /** History deep kind */ HISTORY_DEEP, 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 07c5d722..c27fa999 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 @@ -724,7 +724,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo // TODO: need to make below more clear when // we figure out rest of a pseudostates PseudoStateKind kind = state.getPseudoState() != null ? state.getPseudoState().getKind() : null; - if (kind == PseudoStateKind.CHOICE || kind == PseudoStateKind.HISTORY_SHALLOW + if (kind == PseudoStateKind.CHOICE || kind == PseudoStateKind.JUNCTION || kind == PseudoStateKind.HISTORY_SHALLOW || kind == PseudoStateKind.HISTORY_DEEP) { StateContext stateContext = buildStateContext(Stage.STATE_CHANGED, message, transition, stateMachine); State toState = state.getPseudoState().entry(stateContext); @@ -740,6 +740,16 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo callPreStateChangeInterceptors(toState, message, transition, stateMachine); } + if (kind == PseudoStateKind.JUNCTION) { + while (toState != null && toState.getPseudoState() != null + && toState.getPseudoState().getKind() != PseudoStateKind.INITIAL) { + toState = toState.getPseudoState().entry(stateContext); + } + } + if (kind == PseudoStateKind.JUNCTION) { + callPreStateChangeInterceptors(toState, message, transition, stateMachine); + } + setCurrentState(toState, message, transition, true, stateMachine); } else if (kind == PseudoStateKind.ENTRY) { StateContext stateContext = buildStateContext(Stage.STATE_CHANGED, message, transition, stateMachine); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java index 4ae25f7b..2bfee851 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java @@ -74,9 +74,10 @@ public class StateMachineModelTests { TransitionData transitionData1 = new TransitionData("S1", "S2", null, "E1", null, null, null, null, TransitionKind.EXTERNAL, null); transitions.add(transitionData1); Map>> choices = new HashMap<>(); + Map>> junctions = new HashMap<>(); Map> forks = new HashMap<>(); Map> joins = new HashMap<>(); - TransitionsData transitionsData = new TransitionsData<>(transitions, choices, forks, joins, null, null); + TransitionsData transitionsData = new TransitionsData<>(transitions, choices, junctions, forks, joins, null, null); StateMachineModel stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData, transitionsData); ObjectStateMachineFactory factory = new ObjectStateMachineFactory<>(stateMachineModel); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JunctionStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JunctionStateTests.java new file mode 100644 index 00000000..daaac1db --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JunctionStateTests.java @@ -0,0 +1,281 @@ +/* + * Copyright 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. + * 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.ObjectStateMachine; +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 JunctionStateTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + @SuppressWarnings("unchecked") + public void testFirst() { + context.register(BaseConfig.class, Config1.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("junction", "s30").build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S30)); + } + + @Test + @SuppressWarnings("unchecked") + public void testThen1() { + context.register(BaseConfig.class, Config1.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("junction", "s31").build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S31)); + } + + @Test + @SuppressWarnings("unchecked") + public void testThen2() { + context.register(BaseConfig.class, Config1.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("junction", "s32").build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S32)); + } + + @Test + @SuppressWarnings("unchecked") + public void testLast() { + context.register(BaseConfig.class, Config1.class); + context.refresh(); + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S33)); + } + + @Test + @SuppressWarnings("unchecked") + public void testOnlyLast() { + context.register(BaseConfig.class, Config2.class); + context.refresh(); + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S33)); + } + + @Test + @SuppressWarnings("unchecked") + public void testSubsequentJunctionStates() { + 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("junction", "s2").build()); + + assertThat(machine.getState().getIds(), contains(TestStates.S21)); + } + + @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)) + .junction(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() + .withJunction() + .source(TestStates.S3) + .first(TestStates.S30, s30Guard()) + .then(TestStates.S31, s31Guard()) + .then(TestStates.S32, s32Guard()) + .last(TestStates.S33); + } + + @Bean + public Guard s30Guard() { + return new JunctionGuard("s30"); + } + + @Bean + public Guard s31Guard() { + return new JunctionGuard("s31"); + } + + @Bean + public Guard s32Guard() { + return new JunctionGuard("s32"); + } + + } + + @Configuration + @EnableStateMachine + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.SI) + .states(EnumSet.allOf(TestStates.class)) + .junction(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() + .withJunction() + .source(TestStates.S3) + .last(TestStates.S33); + } + + @Bean + public Guard s30Guard() { + return new JunctionGuard("s30"); + } + + @Bean + public Guard s31Guard() { + return new JunctionGuard("s31"); + } + + @Bean + public Guard s32Guard() { + return new JunctionGuard("s32"); + } + + } + + @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)) + .junction(TestStates.S3) + .junction(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() + .withJunction() + .source(TestStates.S3) + .first(TestStates.S2, s2Guard()) + .last(TestStates.S33) + .and() + .withJunction() + .source(TestStates.S2) + .first(TestStates.S20, s20Guard()) + .last(TestStates.S21); + } + + @Bean + public Guard s2Guard() { + return new JunctionGuard("s2"); + } + + @Bean + public Guard s20Guard() { + return new JunctionGuard("s20"); + } + } + + private static class JunctionGuard implements Guard { + + private final String match; + + public JunctionGuard(String match) { + this.match = match; + } + + @Override + public boolean evaluate(StateContext context) { + return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("junction", String.class)); + } + } + +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/UmlModelParser.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/UmlModelParser.java index ac8b374b..dd3828e2 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/UmlModelParser.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/UmlModelParser.java @@ -106,7 +106,7 @@ public class UmlModelParser { HashMap>> choicesCopy = new HashMap>>(); choicesCopy.putAll(choices); return new DataHolder(new StatesData<>(stateDatas), - new TransitionsData(transitionDatas, choicesCopy, forks, joins, entrys, exits)); + new TransitionsData(transitionDatas, choicesCopy, null, forks, joins, entrys, exits)); } private void handleRegion(Region region) {