Support history pseudostates

- Fixes #50 bug which were revealed by
  trying to use history state concepts.
- Fixes #38 for history features.
- Base functionality for deep and shallow
  histories.
- Some housekeeping and polish before doing
  more system wide spring cleaning.
This commit is contained in:
Janne Valkealahti
2015-04-26 10:14:53 +01:00
parent d31919fd19
commit 9d2d4038e2
10 changed files with 670 additions and 11 deletions

View File

@@ -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<S extends Enum<S>, E extends Enum<E>> exten
for (MachineStackItem<S, E> si : regionStack) {
regions.add(si.machine);
}
RegionState<S, E> rstate = new RegionState<S, E>(null, regions, null, null, null, new DefaultPseudoState(PseudoStateKind.INITIAL));
RegionState<S, E> rstate = new RegionState<S, E>(null, regions, null, null, null,
new DefaultPseudoState<S, E>(PseudoStateKind.INITIAL));
Collection<State<S, E>> states = new ArrayList<State<S, E>>();
states.add(rstate);
EnumStateMachine<S, E> m = new EnumStateMachine<S, E>(states, new ArrayList<Transition<S, E>>(), rstate,
@@ -277,6 +279,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
StateMachineTransitions<S, E> stateMachineTransitions) {
State<S, E> state = null;
State<S, E> initialState = null;
PseudoState<S, E> historyState = null;
Action<S, E> initialAction = null;
Collection<State<S, E>> states = new ArrayList<State<S,E>>();
@@ -287,7 +290,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
StateMachine<S, E> stateMachine = machineMap.get(stateData.getState());
if (stateMachine != null) {
state = new StateMachineState<S, E>(stateData.getState(), stateMachine, stateData.getDeferred(),
stateData.getEntryActions(), stateData.getExitActions(), new DefaultPseudoState(
stateData.getEntryActions(), stateData.getExitActions(), new DefaultPseudoState<S, E>(
PseudoStateKind.INITIAL));
// TODO: below if/else doesn't feel right
if (stateDatas.size() > 1 && stateData.isInitial()) {
@@ -304,6 +307,12 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
pseudoState = new DefaultPseudoState<S, E>(PseudoStateKind.INITIAL);
} else if (stateData.isEnd()) {
pseudoState = new DefaultPseudoState<S, E>(PseudoStateKind.END);
} else if (stateData.getPseudoStateKind() == PseudoStateKind.HISTORY_SHALLOW) {
pseudoState = new HistoryPseudoState<S, E>(PseudoStateKind.HISTORY_SHALLOW);
historyState = pseudoState;
} else if (stateData.getPseudoStateKind() == PseudoStateKind.HISTORY_DEEP) {
pseudoState = new HistoryPseudoState<S, E>(PseudoStateKind.HISTORY_DEEP);
historyState = pseudoState;
} else if (stateData.getPseudoStateKind() == PseudoStateKind.CHOICE) {
continue;
}
@@ -377,6 +386,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
EnumStateMachine<S, E> machine = new EnumStateMachine<S, E>(states, transitions, initialState,
initialTransition, null, defaultExtendedState);
machine.setHistoryState(historyState);
if (contextEvents != null) {
machine.setContextEventsEnabled(contextEvents);
}

View File

@@ -53,6 +53,10 @@ public class DefaultStateConfigurer<S, E>
private S end;
private S history;
private History historyType;
private final Collection<S> choices = new ArrayList<S>();
@Override
@@ -73,6 +77,13 @@ public class DefaultStateConfigurer<S, E>
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<S, E>
return this;
}
@Override
public StateConfigurer<S, E> history(S history, History type) {
this.history = history;
this.historyType = type;
state(history);
return this;
}
private void addIncomplete(Object parent, S state, Collection<E> deferred,
Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions) {
incomplete.add(new StateData<S, E>(parent, region, state, deferred, entryActions, exitActions));

View File

@@ -123,4 +123,31 @@ public interface StateConfigurer<S, E> extends
*/
StateConfigurer<S, E> 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<S, E> 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
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public class HistoryPseudoState<S, E> extends AbstractPseudoState<S, E> {
private State<S, E> 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<S, E> state) {
this.state = state;
}
/**
* Gets the current recorded state.
*
* @return the current recorded state.
*/
public State<S, E> getState() {
return state;
}
@Override
public State<S, E> entry(E event, StateContext<S, E> context) {
return state;
}
}

View File

@@ -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
}

View File

@@ -137,6 +137,8 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
// 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<? extends Action<S, E>> actions = getExitActions();
if (actions != null && !isLocal(context)) {
@@ -178,6 +180,12 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
}
}
private boolean isSubstate(State<S, E> left, State<S, E> right) {
Collection<State<S, E>> c = left.getStates();
c.remove(left);
return c.contains(right);
}
@Override
public String toString() {
return "StateMachineState [getIds()=" + getIds() + ", toString()=" + super.toString() + ", getClass()="

View File

@@ -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<S, E> extends LifecycleObjectSupport
private volatile State<S,E> currentState;
private volatile PseudoState<S, E> history;
private volatile Runnable task;
private final Map<String, StateMachineOnTransitionHandler<S, E>> handlers = new HashMap<String, StateMachineOnTransitionHandler<S,E>>();
@@ -166,6 +170,10 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
return extendedState;
}
public void setHistoryState(PseudoState<S, E> history) {
this.history = history;
}
@Override
public boolean sendEvent(Message<E> event) {
if (isComplete() || !isRunning()) {
@@ -205,12 +213,20 @@ public abstract class AbstractStateMachine<S, E> 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<S, E> extends LifecycleObjectSupport
@Override
protected void doStop() {
super.doStop();
notifyStateMachineStopped(this);
currentState = null;
}
@@ -328,6 +343,20 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
messageHeaders, extendedState, transition, stateMachine);
State<S, E> 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<String, Object>());
StateContext<S, E> stateContext = new DefaultStateContext<S, E>(event != null ? event.getPayload() : null,
messageHeaders, extendedState, transition, stateMachine);
State<S, E> 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<String, Object>());
StateContext<S, E> stateContext = new DefaultStateContext<S, E>(event != null ? event.getPayload() : null,
messageHeaders, extendedState, transition, stateMachine);
State<S, E> 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<S, E> extends LifecycleObjectSupport
return null;
}
public void setCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition, boolean exit, StateMachine<S, E> stateMachine) {
void setCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition, boolean exit, StateMachine<S, E> stateMachine) {
State<S, E> findDeep = findDeepParent(state);
boolean isTargetSubOf = false;
if (transition != null) {
@@ -370,9 +399,12 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
}
State<S, E> 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<S, E> extends LifecycleObjectSupport
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, event, transition, false, stateMachine);
}
}
if (history != null) {
if (history.getKind() == PseudoStateKind.HISTORY_SHALLOW) {
((HistoryPseudoState<S, E>)history).setState(findDeep);
} else if (history.getKind() == PseudoStateKind.HISTORY_DEEP){
((HistoryPseudoState<S, E>)history).setState(state);
}
}
}
void exitCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition, StateMachine<S, E> stateMachine) {
@@ -435,6 +473,7 @@ public abstract class AbstractStateMachine<S, E> 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<S, E> 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) {

View File

@@ -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
}

View File

@@ -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<TestStates,TestEvents> 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<TestStates,TestEvents> 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<TestStates,TestEvents> 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<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> 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<TestStates, TestEvents> 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<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> 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<TestStates, TestEvents> 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<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> 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<TestStates, TestEvents> 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);
}
}
}

View File

@@ -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<TestStates,TestEvents> 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<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
assertThat(machine, notNullValue());
machine.start();
machine.sendEvent(TestEvents.E1);
assertThat(machine.isRunning(), is(true));
State<TestStates, TestEvents> s = machine.getState();
StateMachine<TestStates, TestEvents> m = ((StateMachineState<TestStates, TestEvents>) s).getSubmachine();
boolean r = TestUtils.readField("running", m);
assertThat(r, is(true));
s = m.getState();
m = ((StateMachineState<TestStates, TestEvents>) 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<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
assertThat(machine, notNullValue());
machine.start();
machine.sendEvent(TestEvents.E1);
assertThat(machine.isRunning(), is(true));
State<TestStates, TestEvents> s = machine.getState();
StateMachine<TestStates, TestEvents> m = ((StateMachineState<TestStates, TestEvents>) s).getSubmachine();
boolean r = TestUtils.readField("running", m);
assertThat(r, is(true));
s = m.getState();
m = ((StateMachineState<TestStates, TestEvents>) 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<TestStates,TestEvents> 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<TestStates, TestEvents> s1 = machine.getState();
StateMachine<TestStates, TestEvents> m1 = ((StateMachineState<TestStates, TestEvents>) s1).getSubmachine();
State<TestStates, TestEvents> s2 = m1.getState();
StateMachine<TestStates, TestEvents> m2 = ((StateMachineState<TestStates, TestEvents>) 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<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> 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<TestStates, TestEvents> 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<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> 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<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S2)
.event(TestEvents.E1);
}
}
@Configuration
@EnableStateMachine
static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> 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<TestStates, TestEvents> 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);
}
}