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 5c064750..d8cbc9ea 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 @@ -35,6 +35,7 @@ import org.springframework.statemachine.region.Region; import org.springframework.statemachine.state.ChoicePseudoState; import org.springframework.statemachine.state.DefaultPseudoState; import org.springframework.statemachine.state.EnumState; +import org.springframework.statemachine.state.HistoryPseudoState; import org.springframework.statemachine.state.PseudoState; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.RegionState; @@ -148,7 +149,8 @@ public class EnumStateMachineFactory, E extends Enum> exten for (MachineStackItem si : regionStack) { regions.add(si.machine); } - RegionState rstate = new RegionState(null, regions, null, null, null, new DefaultPseudoState(PseudoStateKind.INITIAL)); + RegionState rstate = new RegionState(null, regions, null, null, null, + new DefaultPseudoState(PseudoStateKind.INITIAL)); Collection> states = new ArrayList>(); states.add(rstate); EnumStateMachine m = new EnumStateMachine(states, new ArrayList>(), rstate, @@ -277,6 +279,7 @@ public class EnumStateMachineFactory, E extends Enum> exten StateMachineTransitions stateMachineTransitions) { State state = null; State initialState = null; + PseudoState historyState = null; Action initialAction = null; Collection> states = new ArrayList>(); @@ -287,7 +290,7 @@ public class EnumStateMachineFactory, E extends Enum> exten StateMachine stateMachine = machineMap.get(stateData.getState()); if (stateMachine != null) { state = new StateMachineState(stateData.getState(), stateMachine, stateData.getDeferred(), - stateData.getEntryActions(), stateData.getExitActions(), new DefaultPseudoState( + stateData.getEntryActions(), stateData.getExitActions(), new DefaultPseudoState( PseudoStateKind.INITIAL)); // TODO: below if/else doesn't feel right if (stateDatas.size() > 1 && stateData.isInitial()) { @@ -304,6 +307,12 @@ public class EnumStateMachineFactory, E extends Enum> exten pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); } else if (stateData.isEnd()) { pseudoState = new DefaultPseudoState(PseudoStateKind.END); + } else if (stateData.getPseudoStateKind() == PseudoStateKind.HISTORY_SHALLOW) { + pseudoState = new HistoryPseudoState(PseudoStateKind.HISTORY_SHALLOW); + historyState = pseudoState; + } else if (stateData.getPseudoStateKind() == PseudoStateKind.HISTORY_DEEP) { + pseudoState = new HistoryPseudoState(PseudoStateKind.HISTORY_DEEP); + historyState = pseudoState; } else if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) { continue; } @@ -377,6 +386,7 @@ public class EnumStateMachineFactory, E extends Enum> exten EnumStateMachine machine = new EnumStateMachine(states, transitions, initialState, initialTransition, null, defaultExtendedState); + machine.setHistoryState(historyState); if (contextEvents != null) { machine.setContextEventsEnabled(contextEvents); } 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 2551fba0..04d20d72 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 @@ -53,6 +53,10 @@ public class DefaultStateConfigurer private S end; + private S history; + + private History historyType; + private final Collection choices = new ArrayList(); @Override @@ -73,6 +77,13 @@ public class DefaultStateConfigurer if (choices.contains(s.getState())) { s.setPseudoStateKind(PseudoStateKind.CHOICE); } + if (s.getState() == history) { + if (History.SHALLOW == historyType) { + s.setPseudoStateKind(PseudoStateKind.HISTORY_SHALLOW); + } else if (History.DEEP == historyType) { + s.setPseudoStateKind(PseudoStateKind.HISTORY_DEEP); + } + } } builder.addStateData(stateDatas); } @@ -152,6 +163,14 @@ public class DefaultStateConfigurer return this; } + @Override + public StateConfigurer history(S history, History type) { + this.history = history; + this.historyType = type; + state(history); + return this; + } + private void addIncomplete(Object parent, S state, Collection deferred, Collection> entryActions, Collection> exitActions) { incomplete.add(new StateData(parent, region, state, deferred, entryActions, exitActions)); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java index e16b4cff..c93cbf4e 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,4 +123,31 @@ public interface StateConfigurer extends */ StateConfigurer choice(S choice); + /** + * Specify a state {@code S} to be history pseudo state. + * + * @param history the history pseudo state + * @param type the history pseudo state type + * @return configurer for chaining + */ + StateConfigurer history(S history, History type); + + /** + * Enumeration of a possible history pseudostate type. + */ + public enum History { + + /** + * Shallow history is a pseudo state representing the most + * recent substate of a submachine. + */ + SHALLOW, + + /** + * Deep history is a shallow history recursively reactivating + * the substates of the most recent substate. + */ + DEEP + } + } 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 new file mode 100644 index 00000000..31f56a5e --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/HistoryPseudoState.java @@ -0,0 +1,67 @@ +/* + * 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 org.springframework.statemachine.StateContext; +import org.springframework.util.Assert; + +/** + * History implementation of a {@link PseudoState}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class HistoryPseudoState extends AbstractPseudoState { + + private State state; + + /** + * Instantiates a new history pseudo state. + * + * @param kind the kind + */ + public HistoryPseudoState(PseudoStateKind kind) { + super(kind); + Assert.isTrue(PseudoStateKind.HISTORY_SHALLOW == kind || PseudoStateKind.HISTORY_DEEP == kind, + "Pseudo state must be either shallow or deep"); + } + + /** + * Sets the current recorded state. + * + * @param state the state + */ + public void setState(State state) { + this.state = state; + } + + /** + * Gets the current recorded state. + * + * @return the current recorded state. + */ + public State getState() { + 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/PseudoStateKind.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/PseudoStateKind.java index d53d496e..bef63133 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 @@ -16,7 +16,7 @@ package org.springframework.statemachine.state; /** - * Defines enumeration of a {@link PseudoState} kind. This is uses within a + * Defines enumeration of a {@link PseudoState} kind. This is used within a * transitive states indicating its kind. * * @author Janne Valkealahti @@ -31,6 +31,12 @@ public enum PseudoStateKind { END, /** Choice kind */ - CHOICE + CHOICE, + + /** History deep kind */ + HISTORY_DEEP, + + /** History shallow kind */ + HISTORY_SHALLOW } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java index f030d3e8..ee55c36e 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java @@ -137,6 +137,8 @@ public class StateMachineState extends AbstractState { // enable default transition and state if (getSubmachine().getState() != null && context.getTransition().getSource().getId() != getSubmachine().getState().getId()) { getSubmachine().stop(); + } else if (!isSubstate(context.getTransition().getTarget(), context.getTransition().getSource())) { + getSubmachine().stop(); } Collection> actions = getExitActions(); if (actions != null && !isLocal(context)) { @@ -178,6 +180,12 @@ public class StateMachineState extends AbstractState { } } + private boolean isSubstate(State left, State right) { + Collection> c = left.getStates(); + c.remove(left); + return c.contains(right); + } + @Override public String toString() { return "StateMachineState [getIds()=" + getIds() + ", toString()=" + super.toString() + ", getClass()=" 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 4c1893fb..938f8a3c 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 @@ -51,6 +51,8 @@ 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.HistoryPseudoState; +import org.springframework.statemachine.state.PseudoState; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.Transition; @@ -94,6 +96,8 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport private volatile State currentState; + private volatile PseudoState history; + private volatile Runnable task; private final Map> handlers = new HashMap>(); @@ -166,6 +170,10 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport return extendedState; } + public void setHistoryState(PseudoState history) { + this.history = history; + } + @Override public boolean sendEvent(Message event) { if (isComplete() || !isRunning()) { @@ -205,12 +213,20 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport region.addStateListener(new StateMachineListenerRelay()); } } + if (state.getPseudoState() != null + && (state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP || state.getPseudoState() + .getKind() == PseudoStateKind.HISTORY_DEEP)) { + history = state.getPseudoState(); + } } } @Override protected void doStart() { - super.doStart(); + // if state is set assume nothing to do + if (currentState != null) { + return; + } registerTriggerListener(); switchToState(initialState, initialEvent, null, this); // TODO: for now execute outside of switchToState @@ -224,7 +240,6 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport @Override protected void doStop() { - super.doStop(); notifyStateMachineStopped(this); currentState = null; } @@ -328,6 +343,20 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport messageHeaders, extendedState, transition, stateMachine); State entry = state.getPseudoState().entry(event.getPayload(), stateContext); setCurrentState(entry, event, transition, true, stateMachine); + } else if (state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.HISTORY_SHALLOW) { + MessageHeaders messageHeaders = event != null ? event.getHeaders() : new MessageHeaders( + new HashMap()); + StateContext stateContext = new DefaultStateContext(event != null ? event.getPayload() : null, + messageHeaders, extendedState, transition, stateMachine); + State entry = state.getPseudoState().entry(event.getPayload(), stateContext); + setCurrentState(entry, event, transition, true, stateMachine); + } else if (state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.HISTORY_DEEP) { + MessageHeaders messageHeaders = event != null ? event.getHeaders() : new MessageHeaders( + new HashMap()); + StateContext stateContext = new DefaultStateContext(event != null ? event.getPayload() : null, + messageHeaders, extendedState, transition, stateMachine); + State entry = state.getPseudoState().entry(event.getPayload(), stateContext); + setCurrentState(entry, event, transition, true, stateMachine); } else { setCurrentState(state, event, transition, true, stateMachine); } @@ -354,7 +383,7 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport return null; } - public void setCurrentState(State state, Message event, Transition transition, boolean exit, StateMachine stateMachine) { + void setCurrentState(State state, Message event, Transition transition, boolean exit, StateMachine stateMachine) { State findDeep = findDeepParent(state); boolean isTargetSubOf = false; if (transition != null) { @@ -370,9 +399,12 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport } State notifyFrom = currentState; currentState = state; + if (!isRunning()) { + start(); + } entryToState(state, event, transition, stateMachine); notifyStateChanged(notifyFrom, state); - } else if (currentState != null && currentState.isSubmachineState()) { + } else if (currentState != null) { if (findDeep != null) { if (exit) { exitCurrentState(state, event, transition, stateMachine); @@ -396,7 +428,13 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport ((AbstractStateMachine)submachine).setCurrentState(state, event, transition, false, stateMachine); } } - + if (history != null) { + if (history.getKind() == PseudoStateKind.HISTORY_SHALLOW) { + ((HistoryPseudoState)history).setState(findDeep); + } else if (history.getKind() == PseudoStateKind.HISTORY_DEEP){ + ((HistoryPseudoState)history).setState(state); + } + } } void exitCurrentState(State state, Message event, Transition transition, StateMachine stateMachine) { @@ -435,6 +473,7 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport } 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; } @@ -467,6 +506,7 @@ public abstract class AbstractStateMachine extends LifecycleObjectSupport if (currentState == transition.getSource() && currentState == transition.getTarget()) { } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { } else if (isComingFromOtherSubmachine) { + } else if (!isSubOfSource && !isSubOfTarget && findDeep2 == null) { } else if (isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { return; } else if (!isSubOfSource && !isSubOfTarget) { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java index 7ab88359..eea9f32b 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java @@ -63,9 +63,9 @@ public abstract class AbstractStateMachineTests { } public enum TestStates { - SI,S1,S2,S3,S4,SF, + SI,S1,S2,S3,S4,SF,SH, S10,S11,S101,S111,S112,S12,S121,S122,S13, - S20,S21,S201,S211, + S20,S21,S201,S211,S212, S1011,S1012,S2011,S2012, S30,S31,S32,S33 } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/HistoryStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/HistoryStateTests.java new file mode 100644 index 00000000..7450e448 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/HistoryStateTests.java @@ -0,0 +1,243 @@ +/* + * 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; +import org.springframework.statemachine.config.configurers.StateConfigurer.History; + +public class HistoryStateTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + @SuppressWarnings("unchecked") + public void testShallowInSubmachine() { + 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); + machine.sendEvent(TestEvents.E4); + + assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21)); + } + + @Test + @SuppressWarnings("unchecked") + public void testDeep() { + context.register(BaseConfig.class, Config2.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); + machine.sendEvent(TestEvents.E4); + + assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21, TestStates.S212)); + } + + @Test + @SuppressWarnings("unchecked") + public void testShallow() { + context.register(BaseConfig.class, Config3.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); + machine.sendEvent(TestEvents.E4); + + assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21, TestStates.S211)); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21) + .history(TestStates.SH, History.SHALLOW); + + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S20) + .target(TestStates.S21) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S1) + .event(TestEvents.E3) + .and() + .withExternal() + .source(TestStates.S1) + .target(TestStates.SH) + .event(TestEvents.E4); + } + + } + + @Configuration + @EnableStateMachine + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21) + .history(TestStates.SH, History.DEEP) + .and() + .withStates() + .parent(TestStates.S21) + .initial(TestStates.S211) + .state(TestStates.S211) + .state(TestStates.S212); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S211) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S211) + .target(TestStates.S212) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S212) + .target(TestStates.S1) + .event(TestEvents.E3) + .and() + .withExternal() + .source(TestStates.S1) + .target(TestStates.SH) + .event(TestEvents.E4); + } + + } + + @Configuration + @EnableStateMachine + static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21) + .history(TestStates.SH, History.SHALLOW) + .and() + .withStates() + .parent(TestStates.S21) + .initial(TestStates.S211) + .state(TestStates.S211) + .state(TestStates.S212); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S211) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S211) + .target(TestStates.S212) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S212) + .target(TestStates.S1) + .event(TestEvents.E3) + .and() + .withExternal() + .source(TestStates.S1) + .target(TestStates.SH) + .event(TestEvents.E4); + } + + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java index 5aade998..724c231a 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java @@ -17,15 +17,25 @@ package org.springframework.statemachine.state; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.Collection; import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.statemachine.AbstractStateMachineTests; import org.springframework.statemachine.EnumStateMachine; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.TestUtils; +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.transition.DefaultExternalTransition; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.trigger.EventTrigger; @@ -38,6 +48,11 @@ import org.springframework.statemachine.trigger.EventTrigger; */ public class SubmachineStateTests extends AbstractStateMachineTests { + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + @Test public void testSimpleSubmachineState() { @@ -80,8 +95,232 @@ public class SubmachineStateTests extends AbstractStateMachineTests { assertThat(state.isSubmachineState(), is(true)); assertThat(state.getIds(), contains(TestStates.S4, TestStates.SI)); + } + @Test + @SuppressWarnings("unchecked") + public void testFromSimpleToOtherSubstate() { + 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); + machine.sendEvent(TestEvents.E4); + assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21)); + } + + @Test + @SuppressWarnings("unchecked") + public void testAllSubmachinesRunningInitialsTakesToDeep() throws Exception { + context.register(BaseConfig.class, Config2.class); + context.refresh(); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(TestEvents.E1); + + assertThat(machine.isRunning(), is(true)); + + State s = machine.getState(); + StateMachine m = ((StateMachineState) s).getSubmachine(); + boolean r = TestUtils.readField("running", m); + assertThat(r, is(true)); + + s = m.getState(); + m = ((StateMachineState) s).getSubmachine(); + r = TestUtils.readField("running", m); + assertThat(r, is(true)); + + assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S20, TestStates.S2011)); + } + + @Test + @SuppressWarnings("unchecked") + public void testAllSubmachinesRunningInitialsNotTakeToDeep() throws Exception { + context.register(BaseConfig.class, Config3.class); + context.refresh(); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + machine.sendEvent(TestEvents.E1); + + assertThat(machine.isRunning(), is(true)); + + State s = machine.getState(); + StateMachine m = ((StateMachineState) s).getSubmachine(); + boolean r = TestUtils.readField("running", m); + assertThat(r, is(true)); + + s = m.getState(); + m = ((StateMachineState) s).getSubmachine(); + r = TestUtils.readField("running", m); + assertThat(r, is(true)); + + assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21, TestStates.S212)); + } + + @Test + @SuppressWarnings("unchecked") + public void testAllSubmachinesStopped() throws Exception { + context.register(BaseConfig.class, Config3.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); + + assertThat(machine.isRunning(), is(true)); + + State s1 = machine.getState(); + StateMachine m1 = ((StateMachineState) s1).getSubmachine(); + + State s2 = m1.getState(); + StateMachine m2 = ((StateMachineState) s2).getSubmachine(); + + machine.sendEvent(TestEvents.E3); + + boolean r1 = TestUtils.readField("running", m1); + assertThat(r1, is(false)); + boolean r2 = TestUtils.readField("running", m2); + assertThat(r2, is(false)); + + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S20) + .target(TestStates.S21) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S1) + .event(TestEvents.E3) + .and() + .withExternal() + .source(TestStates.S1) + .target(TestStates.S21) + .event(TestEvents.E4); + } + + } + + @Configuration + @EnableStateMachine + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21) + .and() + .withStates() + .parent(TestStates.S20) + .initial(TestStates.S2011) + .state(TestStates.S2011) + .state(TestStates.S2012); + + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1); + } + + } + + @Configuration + @EnableStateMachine + static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21) + .and() + .withStates() + .parent(TestStates.S21) + .initial(TestStates.S211) + .state(TestStates.S211) + .state(TestStates.S212); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S212) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S211) + .target(TestStates.S212) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S212) + .target(TestStates.S1) + .event(TestEvents.E3); + } }