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 65edccf8..16c63351 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 @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Stack; import org.springframework.beans.factory.BeanFactory; @@ -33,15 +34,17 @@ import org.springframework.statemachine.config.builders.StateMachineTransitions. 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.ChoicePseudoState.ChoiceStateData; import org.springframework.statemachine.state.DefaultPseudoState; import org.springframework.statemachine.state.EnumState; +import org.springframework.statemachine.state.ForkPseudoState; import org.springframework.statemachine.state.HistoryPseudoState; +import org.springframework.statemachine.state.JoinPseudoState; import org.springframework.statemachine.state.PseudoState; 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; @@ -152,6 +155,20 @@ public class EnumStateMachineFactory, E extends Enum> exten new DefaultPseudoState(PseudoStateKind.INITIAL)); if (stateData != null) { stateMap.put(stateData.getState(), rstate); + } else { + // TODO: don't like that we create a last machine here + Collection> states = new ArrayList>(); + states.add(rstate); + EnumStateMachine m = new EnumStateMachine(states, new ArrayList>(), rstate, + null, null, defaultExtendedState); + if (contextEvents != null) { + m.setContextEventsEnabled(contextEvents); + } + if (getBeanFactory() != null) { + m.setBeanFactory(getBeanFactory()); + } + m.afterPropertiesSet(); + machine = m; } } else { machine = buildMachine(machineMap, stateMap, stateDatas, transitionsData, getBeanFactory(), @@ -306,6 +323,10 @@ public class EnumStateMachineFactory, E extends Enum> exten } else if (stateData.getPseudoStateKind() == PseudoStateKind.HISTORY_DEEP) { pseudoState = new HistoryPseudoState(PseudoStateKind.HISTORY_DEEP); historyState = pseudoState; + } else if (stateData.getPseudoStateKind() == PseudoStateKind.JOIN) { + continue; + } else if (stateData.getPseudoStateKind() == PseudoStateKind.FORK) { + continue; } else if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) { continue; } @@ -334,6 +355,41 @@ public class EnumStateMachineFactory, E extends Enum> exten stateData.getEntryActions(), stateData.getExitActions(), pseudoState); states.add(state); stateMap.put(stateData.getState(), state); + } else if (stateData.getPseudoStateKind() == PseudoStateKind.FORK) { + S s = stateData.getState(); + List list = stateMachineTransitions.getForks().get(s); + List> forks = new ArrayList>(); + for (S fs : list) { + forks.add(stateMap.get(fs)); + } + PseudoState pseudoState = new ForkPseudoState(forks); + state = new EnumState(stateData.getState(), stateData.getDeferred(), + stateData.getEntryActions(), stateData.getExitActions(), pseudoState); + states.add(state); + stateMap.put(stateData.getState(), state); + } else if (stateData.getPseudoStateKind() == PseudoStateKind.JOIN) { + S s = stateData.getState(); + List list = stateMachineTransitions.getJoins().get(s); + List> joins = new ArrayList>(); + for (S fs : list) { + joins.add(stateMap.get(fs)); + } + JoinPseudoState pseudoState = new JoinPseudoState(joins); + state = new EnumState(stateData.getState(), stateData.getDeferred(), + stateData.getEntryActions(), stateData.getExitActions(), pseudoState); + states.add(state); + stateMap.put(stateData.getState(), state); + + // find joins sources and associate + for (Entry> e : stateMap.entrySet()) { + State value = e.getValue(); + if (value.isOrthogonal()) { + Collection> states2 = value.getStates(); + if (states2.containsAll(joins)) { + ((RegionState)value).setJoin(pseudoState); + } + } + } } } 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 231dd5db..82ad3b5f 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 @@ -30,10 +30,14 @@ import org.springframework.statemachine.config.common.annotation.ObjectPostProce 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.DefaultForkTransitionConfigurer; import org.springframework.statemachine.config.configurers.DefaultInternalTransitionConfigurer; +import org.springframework.statemachine.config.configurers.DefaultJoinTransitionConfigurer; import org.springframework.statemachine.config.configurers.DefaultLocalTransitionConfigurer; import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer; +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.LocalTransitionConfigurer; import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.transition.TransitionKind; @@ -53,6 +57,8 @@ public class StateMachineTransitionBuilder private final Collection> transitionData = new ArrayList>(); private final Map>> choices = new HashMap>>(); + private final Map> forks = new HashMap>(); + private final Map> joins = new HashMap>(); public StateMachineTransitionBuilder() { super(); @@ -69,7 +75,7 @@ public class StateMachineTransitionBuilder @Override protected StateMachineTransitions performBuild() throws Exception { - return new StateMachineTransitions(transitionData, choices); + return new StateMachineTransitions(transitionData, choices, forks, joins); } @Override @@ -92,6 +98,16 @@ public class StateMachineTransitionBuilder return apply(new DefaultChoiceTransitionConfigurer()); } + @Override + public ForkTransitionConfigurer withFork() throws Exception { + return apply(new DefaultForkTransitionConfigurer()); + } + + @Override + public JoinTransitionConfigurer withJoin() throws Exception { + return apply(new DefaultJoinTransitionConfigurer()); + } + 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)); @@ -101,4 +117,12 @@ public class StateMachineTransitionBuilder this.choices.put(source, choices); } + public void addFork(S source, List targets) { + this.forks.put(source, targets); + } + + public void addJoin(S target, List sources) { + this.joins.put(target, sources); + } + } 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 db79099d..fd7362af 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 @@ -17,7 +17,9 @@ 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.ForkTransitionConfigurer; import org.springframework.statemachine.config.configurers.InternalTransitionConfigurer; +import org.springframework.statemachine.config.configurers.JoinTransitionConfigurer; import org.springframework.statemachine.config.configurers.LocalTransitionConfigurer; /** @@ -65,9 +67,25 @@ public interface StateMachineTransitionConfigurer { /** * Gets a configurer for transition from a choice pseudostate. * - * @return {@link LocalTransitionConfigurer} for chaining + * @return {@link ChoiceTransitionConfigurer} for chaining * @throws Exception if configuration error happens */ ChoiceTransitionConfigurer withChoice() throws Exception; + /** + * Gets a configurer for transition from a fork pseudostate. + * + * @return {@link ForkTransitionConfigurer} for chaining + * @throws Exception if configuration error happens + */ + ForkTransitionConfigurer withFork() throws Exception; + + /** + * Gets a configurer for transition from a join pseudostate. + * + * @return {@link JoinTransitionConfigurer} for chaining + * @throws Exception if configuration error happens + */ + JoinTransitionConfigurer withJoin() 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 3b743812..be494231 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 @@ -35,10 +35,15 @@ public class StateMachineTransitions { private Collection> transitions; private Map>> choices; + private Map> forks; + private Map> joins; - public StateMachineTransitions(Collection> transitions, Map>> choices) { + public StateMachineTransitions(Collection> transitions, + Map>> choices, Map> forks, Map> joins) { this.transitions = transitions; this.choices = choices; + this.forks = forks; + this.joins = joins; } public Collection> getTransitions() { @@ -49,6 +54,14 @@ public class StateMachineTransitions { return choices; } + public Map> getForks() { + return forks; + } + + public Map> getJoins() { + return joins; + } + public static class TransitionData { S source; S target; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultForkTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultForkTransitionConfigurer.java new file mode 100644 index 00000000..a923dab3 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultForkTransitionConfigurer.java @@ -0,0 +1,59 @@ +/* + * 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.common.annotation.AnnotationConfigurerAdapter; + +/** + * Default implementation of a {@link ForkTransitionConfigurer}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class DefaultForkTransitionConfigurer + extends AnnotationConfigurerAdapter, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> + implements ForkTransitionConfigurer { + + private S source; + + private final List targets = new ArrayList(); + + @Override + public void configure(StateMachineTransitionBuilder builder) throws Exception { + builder.addFork(source, targets); + } + + @Override + public ForkTransitionConfigurer source(S source) { + this.source = source; + return this; + } + + @Override + public ForkTransitionConfigurer target(S target) { + targets.add(target); + return this; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultJoinTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultJoinTransitionConfigurer.java new file mode 100644 index 00000000..ef2e1ae6 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultJoinTransitionConfigurer.java @@ -0,0 +1,59 @@ +/* + * 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.common.annotation.AnnotationConfigurerAdapter; + +/** + * Default implementation of a {@link JoinTransitionConfigurer}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class DefaultJoinTransitionConfigurer + extends AnnotationConfigurerAdapter, StateMachineTransitionConfigurer, StateMachineTransitionBuilder> + implements JoinTransitionConfigurer { + + private S target; + + private final List sources = new ArrayList(); + + @Override + public void configure(StateMachineTransitionBuilder builder) throws Exception { + builder.addJoin(target, sources); + } + + @Override + public JoinTransitionConfigurer source(S source) { + this.sources.add(source); + return this; + } + + @Override + public JoinTransitionConfigurer target(S target) { + this.target = target; + 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 04d20d72..cccd992f 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 @@ -59,6 +59,10 @@ public class DefaultStateConfigurer private final Collection choices = new ArrayList(); + private final Collection forks = new ArrayList(); + + private final Collection joins = new ArrayList(); + @Override public void configure(StateMachineStateBuilder builder) throws Exception { // before passing state datas to builder, update structure @@ -76,6 +80,10 @@ public class DefaultStateConfigurer } if (choices.contains(s.getState())) { s.setPseudoStateKind(PseudoStateKind.CHOICE); + } else if (forks.contains(s.getState())) { + s.setPseudoStateKind(PseudoStateKind.FORK); + } else if (joins.contains(s.getState())) { + s.setPseudoStateKind(PseudoStateKind.JOIN); } if (s.getState() == history) { if (History.SHALLOW == historyType) { @@ -163,6 +171,20 @@ public class DefaultStateConfigurer return this; } + @Override + public StateConfigurer fork(S fork) { + state(fork); + forks.add(fork); + return this; + } + + @Override + public StateConfigurer join(S join) { + state(join); + joins.add(join); + return this; + } + @Override public StateConfigurer history(S history, History type) { this.history = history; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ForkTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ForkTransitionConfigurer.java new file mode 100644 index 00000000..0ef35950 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ForkTransitionConfigurer.java @@ -0,0 +1,50 @@ +/* + * 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.transition.Transition; + +/** + * {@code TransitionConfigurer} interface for configuring {@link Transition} + * from a fork pseudo state. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface ForkTransitionConfigurer + extends AnnotationConfigurerBuilder> { + + /** + * Specify a source state {@code S} for this {@link Transition}. + * + * @param source the source state {@code S} + * @return configurer for chaining + */ + ForkTransitionConfigurer source(S source); + + /** + * Specify a target state {@code S} for this {@link Transition}. + * + * @param target the target state {@code S} + * @return configurer for chaining + */ + ForkTransitionConfigurer target(S target); + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/JoinTransitionConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/JoinTransitionConfigurer.java new file mode 100644 index 00000000..8f477315 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/JoinTransitionConfigurer.java @@ -0,0 +1,50 @@ +/* + * 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.transition.Transition; + +/** + * {@code TransitionConfigurer} interface for configuring {@link Transition} + * from a join pseudo state. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface JoinTransitionConfigurer + extends AnnotationConfigurerBuilder> { + + /** + * Specify a source state {@code S} for this {@link Transition}. + * + * @param source the source state {@code S} + * @return configurer for chaining + */ + JoinTransitionConfigurer source(S source); + + /** + * Specify a target state {@code S} for this {@link Transition}. + * + * @param target the target state {@code S} + * @return configurer for chaining + */ + JoinTransitionConfigurer target(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 c93cbf4e..73278b07 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 @@ -123,6 +123,22 @@ public interface StateConfigurer extends */ StateConfigurer choice(S choice); + /** + * Specify a state {@code S} to be fork pseudo state. + * + * @param fork the fork pseudo state + * @return configurer for chaining + */ + StateConfigurer fork(S fork); + + /** + * Specify a state {@code S} to be join pseudo state. + * + * @param join the join pseudo state + * @return configurer for chaining + */ + StateConfigurer join(S join); + /** * Specify a state {@code S} to be history pseudo state. * 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 1c6e755c..514ed306 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 @@ -29,6 +29,8 @@ public abstract class AbstractPseudoState implements PseudoState { private final PseudoStateKind kind; + private final CompositePseudoStateListener pseudoStateListener = new CompositePseudoStateListener(); + /** * Instantiates a new abstract pseudo state. * @@ -44,8 +46,26 @@ public abstract class AbstractPseudoState implements PseudoState { } @Override - public State entry(E event, StateContext context) { + public State entry(StateContext context) { return null; } + + @Override + public void exit(StateContext context) { + } + + @Override + public void addPseudoStateListener(PseudoStateListener listener) { + pseudoStateListener.register(listener); + } + + /** + * Notify all {@link PseudoStateListener}s of a new context. + * + * @param context the new context + */ + protected void notifyContext(PseudoStateContext context) { + pseudoStateListener.onContext(context); + } } 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 245379da..7cc60f8b 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 @@ -42,7 +42,7 @@ public class ChoicePseudoState implements PseudoState { } @Override - public State entry(E event, StateContext context) { + public State entry(StateContext context) { State s = null; for (ChoiceStateData c : choices) { s = c.getState(); @@ -52,6 +52,14 @@ public class ChoicePseudoState implements PseudoState { } return s; } + + @Override + public void exit(StateContext context) { + } + + @Override + public void addPseudoStateListener(PseudoStateListener listener) { + } public static class ChoiceStateData { private final State state; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CompositePseudoStateListener.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CompositePseudoStateListener.java new file mode 100644 index 00000000..96bc4389 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/CompositePseudoStateListener.java @@ -0,0 +1,33 @@ +/* + * 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.Iterator; + +import org.springframework.statemachine.listener.AbstractCompositeListener; + +public class CompositePseudoStateListener extends AbstractCompositeListener> implements + PseudoStateListener { + + @Override + public void onContext(PseudoStateContext context) { + for (Iterator> iterator = getListeners().reverse(); iterator.hasNext();) { + PseudoStateListener listener = iterator.next(); + listener.onContext(context); + } + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/DefaultPseudoStateContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/DefaultPseudoStateContext.java new file mode 100644 index 00000000..c7b11017 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/DefaultPseudoStateContext.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Default implementation of a {@link PseudoStateContext}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class DefaultPseudoStateContext implements PseudoStateContext { + + private final PseudoState pseudoState; + + private final PseudoAction pseudoAction; + + /** + * Instantiates a new default pseudo state context. + * + * @param pseudoState the pseudo state + * @param pseudoAction the pseudo action + */ + public DefaultPseudoStateContext(PseudoState pseudoState, PseudoAction pseudoAction) { + this.pseudoState = pseudoState; + this.pseudoAction = pseudoAction; + } + + @Override + public PseudoState getPseudoState() { + return pseudoState; + } + + @Override + public PseudoAction getPseudoAction() { + return pseudoAction; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ForkPseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ForkPseudoState.java new file mode 100644 index 00000000..9369f32c --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ForkPseudoState.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** + * Fork implementation of a {@link PseudoState}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class ForkPseudoState extends AbstractPseudoState { + + private final List> forks; + + public ForkPseudoState(List> forks) { + super(PseudoStateKind.FORK); + this.forks = forks; + } + + @Override + public State entry(StateContext context) { + return null; + } + + public List> getForks() { + return forks; + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/HistoryPseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/HistoryPseudoState.java index 31f56a5e..7ec293f2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/HistoryPseudoState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/HistoryPseudoState.java @@ -41,6 +41,11 @@ public class HistoryPseudoState extends AbstractPseudoState { "Pseudo state must be either shallow or deep"); } + @Override + public State entry(StateContext context) { + return state; + } + /** * Sets the current recorded state. * @@ -59,9 +64,4 @@ public class HistoryPseudoState extends AbstractPseudoState { return state; } - @Override - public State entry(E event, StateContext context) { - return state; - } - } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JoinPseudoState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JoinPseudoState.java new file mode 100644 index 00000000..7e176ad7 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/JoinPseudoState.java @@ -0,0 +1,85 @@ +/* + * 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.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.PseudoStateContext.PseudoAction; + +/** + * Join implementation of a {@link PseudoState}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class JoinPseudoState extends AbstractPseudoState { + + private final List> joins; + + private volatile JoinTracker tracker; + + public JoinPseudoState(List> joins) { + super(PseudoStateKind.JOIN); + this.joins = joins; + } + + @Override + public State entry(StateContext context) { + tracker = new JoinTracker(this, joins); + context.getStateMachine().addStateListener(tracker); + return null; + } + + @Override + public void exit(StateContext context) { + tracker = null; + } + + public List> getJoins() { + return joins; + } + + private class JoinTracker extends StateMachineListenerAdapter { + + private final PseudoState pseudoState; + private final List> track; + // TOOO use flat till we can unregister listener + private boolean done = false; + + public JoinTracker(PseudoState pseudoState, List> track) { + this.pseudoState = pseudoState; + this.track = track; + } + + @Override + public void stateChanged(State from, State to) { + if (done) { + return; + } + track.remove(to); + if (track.size() == 0) { + done = true; + notifyContext(new DefaultPseudoStateContext(pseudoState, PseudoAction.JOIN_COMPLETED)); + } + } + + } + +} 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 d93866dc..312f91cd 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 @@ -46,10 +46,23 @@ public interface PseudoState { * 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); + State entry(StateContext context); + + /** + * Initiate an exit sequence for the state. + * + * @param context the context + */ + void exit(StateContext context); + + /** + * Registers a new {@link PseudoStateListener}. + * + * @param listener the listener + */ + void addPseudoStateListener(PseudoStateListener listener); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateContext.java new file mode 100644 index 00000000..375f2642 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateContext.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * Context object using in {@link PseudoStateListener}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface PseudoStateContext { + + /** + * Gets the pseudo state. + * + * @return the pseudo state + */ + PseudoState getPseudoState(); + + /** + * Gets the pseudo action. + * + * @return the pseudo action + */ + PseudoAction getPseudoAction(); + + /** + * The PseudoAction enumeration. + */ + public enum PseudoAction { + + /** + * Indication that states has been joined. + */ + JOIN_COMPLETED; + } +} 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 bef63133..cba27e30 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 @@ -37,6 +37,12 @@ public enum PseudoStateKind { HISTORY_DEEP, /** History shallow kind */ - HISTORY_SHALLOW + HISTORY_SHALLOW, + + /** Fork kind */ + FORK, + + /** Join kind */ + JOIN } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateListener.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateListener.java new file mode 100644 index 00000000..2e4ef198 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateListener.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * {@code PseudoStateListener} for various pseudo state events. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface PseudoStateListener { + + /** + * Called when {@link PseudoState} want to notify of + * a new {@link PseudoStateContext}. + * + * @param context the context + */ + void onContext(PseudoStateContext context); + +} 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 db9ef6dd..8183c15b 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 @@ -33,6 +33,8 @@ import org.springframework.statemachine.region.Region; */ public class RegionState extends AbstractState { + private JoinPseudoState join; + /** * Instantiates a new region state. * @@ -96,13 +98,13 @@ public class RegionState extends AbstractState { @Override public boolean sendEvent(Message event) { + boolean accept = false; if (getRegions() != null) { for (Region r : getRegions()) { - r.sendEvent(event); + accept |= r.sendEvent(event); } - return true; } - return false; + return accept; } @Override @@ -121,6 +123,9 @@ public class RegionState extends AbstractState { @Override public void entry(StateContext context) { + if (join != null) { + join.entry(context); + } Collection> actions = getEntryActions(); if (actions != null) { for (Action action : actions) { @@ -144,7 +149,9 @@ public class RegionState extends AbstractState { @Override public Collection getIds() { ArrayList ids = new ArrayList(); - ids.add(getId()); + if (getId() != null) { + ids.add(getId()); + } for (Region r : getRegions()) { State s = r.getState(); if (s != null) { @@ -164,4 +171,14 @@ public class RegionState extends AbstractState { return states; } + public void setJoin(JoinPseudoState join) { + this.join = join; + } + + @Override + public String toString() { + return "RegionState [getIds()=" + getIds() + ", getClass()=" + getClass() + ", hashCode()=" + hashCode() + + ", toString()=" + super.toString() + "]"; + } + } 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 cfe8c29e..1e44de11 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 @@ -49,9 +49,12 @@ import org.springframework.statemachine.processor.StateMachineOnTransitionHandle import org.springframework.statemachine.processor.StateMachineRuntime; import org.springframework.statemachine.region.Region; import org.springframework.statemachine.state.AbstractState; +import org.springframework.statemachine.state.ForkPseudoState; import org.springframework.statemachine.state.HistoryPseudoState; import org.springframework.statemachine.state.PseudoState; +import org.springframework.statemachine.state.PseudoStateContext; import org.springframework.statemachine.state.PseudoStateKind; +import org.springframework.statemachine.state.PseudoStateListener; import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.transition.TransitionKind; @@ -60,6 +63,7 @@ import org.springframework.statemachine.trigger.TimerTrigger; import org.springframework.statemachine.trigger.Trigger; import org.springframework.statemachine.trigger.TriggerListener; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Base implementation of a {@link StateMachine} loosely modelled from UML state @@ -222,6 +226,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo return; } registerTriggerListener(); + registerPseudoStateListener(); switchToState(initialState, initialEvent, null, this); // TODO: for now execute outside of switchToState if (initialTransition != null) { @@ -278,6 +283,10 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo for (State s : all) { buf.append(s.getId() + " "); } + buf.append(" / "); + if (currentState != null) { + buf.append(StringUtils.collectionToCommaDelimitedString(currentState.getIds())); + } return buf.toString(); } @@ -322,8 +331,13 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo if (kind == PseudoStateKind.CHOICE || kind == PseudoStateKind.HISTORY_SHALLOW || kind == PseudoStateKind.HISTORY_DEEP) { StateContext stateContext = buildStateContext(message, transition, stateMachine); - State toState = state.getPseudoState().entry(message.getPayload(), stateContext); + State toState = state.getPseudoState().entry(stateContext); setCurrentState(toState, message, transition, true, stateMachine); + } else if (kind == PseudoStateKind.FORK) { + ForkPseudoState fps = (ForkPseudoState) state.getPseudoState(); + for (State ss : fps.getForks()) { + setCurrentState(ss, message, transition, false, stateMachine); + } } else { setCurrentState(state, message, transition, true, stateMachine); } @@ -341,6 +355,32 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } } + private void registerPseudoStateListener() { + for (State state : states) { + PseudoState p = state.getPseudoState(); + if (p != null) { + p.addPseudoStateListener(new PseudoStateListener() { + @Override + public void onContext(PseudoStateContext context) { + PseudoState pseudoState = context.getPseudoState(); + State toState = findStateWithPseudoState(pseudoState); + pseudoState.exit(null); + switchToState(toState, null, null, AbstractStateMachine.this); + } + }); + } + } + } + + private State findStateWithPseudoState(PseudoState pseudoState) { + for (State s : states) { + if (s.getPseudoState() == pseudoState) { + return s; + } + } + return null; + } + private StateContext buildStateContext(Message message, Transition transition, StateMachine stateMachine) { E event = message != null ? message.getPayload() : null; MessageHeaders messageHeaders = message != null ? message.getHeaders() : new MessageHeaders( @@ -384,22 +424,48 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo exitCurrentState(state, message, transition, stateMachine); } if (currentState == findDeep) { - StateMachine submachine = ((AbstractState)currentState).getSubmachine(); - if (submachine.getState() == state) { - if (currentState == findDeep) { - if (isTargetSubOf) { - entryToState(currentState, message, transition, stateMachine); + + if (currentState.isSubmachineState()) { + StateMachine submachine = ((AbstractState)currentState).getSubmachine(); + if (submachine.getState() == state) { + if (currentState == findDeep) { + if (isTargetSubOf) { + entryToState(currentState, message, transition, stateMachine); + } + currentState = findDeep; + ((AbstractStateMachine)submachine).setCurrentState(state, message, transition, false, stateMachine); + return; } - currentState = findDeep; - ((AbstractStateMachine)submachine).setCurrentState(state, message, transition, false, stateMachine); - return; + } + } else if (currentState.isOrthogonal()) { + Collection> regions = ((AbstractState)currentState).getRegions(); + for (Region region : regions) { + if (region.getState() == state) { + if (currentState == findDeep) { + if (isTargetSubOf) { + entryToState(currentState, message, transition, stateMachine); + } + currentState = findDeep; + ((AbstractStateMachine)region).setCurrentState(state, message, transition, false, stateMachine); + return; + } + } + } } } currentState = findDeep; entryToState(currentState, message, transition, stateMachine); - StateMachine submachine = ((AbstractState)currentState).getSubmachine(); - ((AbstractStateMachine)submachine).setCurrentState(state, message, transition, false, stateMachine); + + if (currentState.isSubmachineState()) { + StateMachine submachine = ((AbstractState)currentState).getSubmachine(); + ((AbstractStateMachine)submachine).setCurrentState(state, message, transition, false, stateMachine); + } else if (currentState.isOrthogonal()) { + Collection> regions = ((AbstractState)currentState).getRegions(); + for (Region region : regions) { + ((AbstractStateMachine)region).setCurrentState(state, message, transition, false, stateMachine); + } + } } } if (history != null) { @@ -424,33 +490,38 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } } - private void exitFromState(State state, Message message, Transition transition, StateMachine stateMachine) { + private void exitFromState(State state, Message message, Transition transition, + StateMachine stateMachine) { if (state == null) { return; } log.trace("Trying Exit state=[" + state + "]"); StateContext stateContext = buildStateContext(message, transition, stateMachine); - State findDeep = findDeepParent(transition.getTarget()); - boolean isTargetSubOfOtherState = findDeep != null && findDeep != currentState; - boolean isTargetSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), transition.getTarget()); - boolean isSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), currentState); - boolean isSubOfTarget = StateMachineUtils.isSubstate(transition.getTarget(), currentState); + if (transition != null) { - // TODO: this and entry below should be done via a separate - // voter of some sort which would reveal transition path - // we could make a choice on. - if (currentState == transition.getSource() && currentState == transition.getTarget()) { - } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getSource()) { - } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { - } else if (isTargetSubOfOtherState) { - } else if (!isSubOfSource && !isSubOfTarget && findDeep == null) { - } else if (!isSubOfSource && !isSubOfTarget) { - return; - } + State findDeep = findDeepParent(transition.getTarget()); + boolean isTargetSubOfOtherState = findDeep != null && findDeep != currentState; + boolean isTargetSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), transition.getTarget()); + boolean isSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), currentState); + boolean isSubOfTarget = StateMachineUtils.isSubstate(transition.getTarget(), currentState); + + // TODO: this and entry below should be done via a separate + // voter of some sort which would reveal transition path + // we could make a choice on. + if (currentState == transition.getSource() && currentState == transition.getTarget()) { + } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getSource()) { + } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { + } else if (isTargetSubOfOtherState) { + } else if (!isSubOfSource && !isSubOfTarget && findDeep == null) { + } else if (!isSubOfSource && !isSubOfTarget) { + return; + } + + if (transition.getSource() == currentState && isTargetSubOfSource) { + return; + } - if (transition.getSource() == currentState && isTargetSubOfSource) { - return; } log.debug("Exit state=[" + state + "]"); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java index c9086dab..2b4e06e3 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java @@ -52,8 +52,8 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup */ protected StateMachineEventPublisher getStateMachineEventPublisher() { if(stateMachineEventPublisher == null && getBeanFactory() != null) { - if(log.isDebugEnabled()) { - log.debug("getting stateMachineEventPublisher service from bean factory " + getBeanFactory()); + if(log.isTraceEnabled()) { + log.trace("getting stateMachineEventPublisher service from bean factory " + getBeanFactory()); } stateMachineEventPublisher = StateMachineContextUtils.getEventPublisher(getBeanFactory()); } 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 764d759a..08bbc051 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 @@ -219,9 +219,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { assertThat(exitActionS112.stateContexts.size(), is(0)); } - // effectively broken now until we get more fixes - // due to work with region fork/join - //@Test + @Test public void testMultiRegion() throws Exception { context.register(BaseConfig.class, StateMachineEventPublisherConfiguration.class, Config1.class); context.refresh(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateMachineTests.java index c2b03787..7e82fb76 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateMachineTests.java @@ -16,6 +16,7 @@ package org.springframework.statemachine; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -100,6 +101,24 @@ public class StateMachineTests extends AbstractStateMachineTests { assertThat(testAction2.stateContexts.size(), is(timedTriggered)); } + @Test + @SuppressWarnings("unchecked") + public void testForkJoin() { + context.register(BaseConfig.class, Config3.class); + context.refresh(); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.SI)); + machine.sendEvent(TestEvents.E1); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S20, TestStates.S30)); + machine.sendEvent(TestEvents.E2); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S21, TestStates.S30)); + machine.sendEvent(TestEvents.E3); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S4)); + } + private static class LoggingAction implements Action { private static final Log log = LogFactory.getLog(StateMachineTests.LoggingAction.class); @@ -237,4 +256,68 @@ public class StateMachineTests extends AbstractStateMachineTests { } + @Configuration + @EnableStateMachine + static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.SI) + .state(TestStates.SI) + .fork(TestStates.S1) + .state(TestStates.S2) + .end(TestStates.SF) + .join(TestStates.S3) + .state(TestStates.S4) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S30) + .state(TestStates.S30) + .state(TestStates.S31); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.SI) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S20) + .target(TestStates.S21) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S30) + .target(TestStates.S31) + .event(TestEvents.E3) + .and() + .withFork() + .source(TestStates.S1) + .target(TestStates.S20) + .target(TestStates.S30) + .and() + .withJoin() + .source(TestStates.S21) + .source(TestStates.S31) + .target(TestStates.S3) + .and() + .withExternal() + .source(TestStates.S3) + .target(TestStates.S4); + } + + } + } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/ForkStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/ForkStateTests.java new file mode 100644 index 00000000..86768c27 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/ForkStateTests.java @@ -0,0 +1,133 @@ +/* + * 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.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +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.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; + +public class ForkStateTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + @SuppressWarnings("unchecked") + public void testForkEventPassed() { + context.register(BaseConfig.class, Config1.class); + context.refresh(); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + TestEntryAction s20EntryAction = context.getBean("s20EntryAction", TestEntryAction.class); + TestEntryAction s21EntryAction = context.getBean("s21EntryAction", TestEntryAction.class); + TestEntryAction s30EntryAction = context.getBean("s30EntryAction", TestEntryAction.class); + TestEntryAction s31EntryAction = context.getBean("s31EntryAction", TestEntryAction.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("foo", "bar").build()); + + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S21, TestStates.S31)); + assertThat(s20EntryAction.stateContexts.size(), is(1)); + assertThat(s21EntryAction.stateContexts.size(), is(1)); + assertThat(s30EntryAction.stateContexts.size(), is(1)); + assertThat(s31EntryAction.stateContexts.size(), is(1)); + assertThat((String)s20EntryAction.stateContexts.get(0).getMessageHeader("foo"), nullValue()); + assertThat((String)s21EntryAction.stateContexts.get(0).getMessageHeader("foo"), is("bar")); + assertThat((String)s30EntryAction.stateContexts.get(0).getMessageHeader("foo"), nullValue()); + assertThat((String)s31EntryAction.stateContexts.get(0).getMessageHeader("foo"), is("bar")); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.SI) + .fork(TestStates.S1) + .state(TestStates.SI) + .state(TestStates.S2) + .end(TestStates.SF) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20, s20EntryAction(), null) + .state(TestStates.S21, s21EntryAction(), null) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S30) + .state(TestStates.S30, s30EntryAction(), null) + .state(TestStates.S31, s31EntryAction(), null); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.SI) + .target(TestStates.S1) + .event(TestEvents.E1) + .and() + .withFork() + .source(TestStates.S1) + .target(TestStates.S21) + .target(TestStates.S31); + } + + @Bean + public TestEntryAction s20EntryAction() { + return new TestEntryAction(); + } + + @Bean + public TestEntryAction s21EntryAction() { + return new TestEntryAction(); + } + + @Bean + public TestEntryAction s30EntryAction() { + return new TestEntryAction(); + } + + @Bean + public TestEntryAction s31EntryAction() { + return new TestEntryAction(); + } + + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JoinStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JoinStateTests.java new file mode 100644 index 00000000..00ffbcf3 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/JoinStateTests.java @@ -0,0 +1,114 @@ +/* + * 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 org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.EnumStateMachine; +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; + +public class JoinStateTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + @SuppressWarnings("unchecked") + public void testJoin() { + 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(TestEvents.E1); + machine.sendEvent(TestEvents.E2); + machine.sendEvent(TestEvents.E3); + + assertThat(machine.getState().getIds(), contains(TestStates.S4)); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.SI) + .state(TestStates.SI) + .state(TestStates.S2) + .end(TestStates.SF) + .join(TestStates.S3) + .state(TestStates.S4) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S30) + .state(TestStates.S30) + .state(TestStates.S31); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.SI) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S20) + .target(TestStates.S21) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S30) + .target(TestStates.S31) + .event(TestEvents.E3) + .and() + .withJoin() + .source(TestStates.S21) + .source(TestStates.S31) + .target(TestStates.S3) + .and() + .withExternal() + .source(TestStates.S3) + .target(TestStates.S4); + } + + } + +} diff --git a/spring-statemachine-core/src/test/resources/log4j.properties b/spring-statemachine-core/src/test/resources/log4j.properties index 4589b1e9..c7fdcd54 100644 --- a/spring-statemachine-core/src/test/resources/log4j.properties +++ b/spring-statemachine-core/src/test/resources/log4j.properties @@ -4,5 +4,5 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} [%t] - %m%n -log4j.category.org.springframework.statemachine=TRACE +log4j.category.org.springframework.statemachine=DEBUG