Fix state handling with substates

- Fixes #23
- Transition with submachine now exists and enters
  super state if transition is external.
This commit is contained in:
Janne Valkealahti
2015-03-15 11:15:37 +00:00
parent 3e5efefc33
commit 9235345a42
6 changed files with 109 additions and 10 deletions

View File

@@ -127,6 +127,7 @@ public class EnumStateMachineFactory<S extends Enum<S>, E extends Enum<E>> exten
Collection<StateData<S, E>> stateDatas = popSameParents(stateStack);
Collection<TransitionData<S, E>> transitionsData = getTransitionData(iterator.hasNext(), stateDatas);
// Collection<TransitionData<S, E>> transitionsData = getTransitionData(false, null);
machine = buildMachine(machineMap, stateMap, stateDatas, transitionsData, getBeanFactory());
// TODO: last part in if feels a bit hack

View File

@@ -204,11 +204,21 @@ public abstract class AbstractState<S, E> implements State<S, E> {
return submachine != null;
}
protected StateMachine<S, E> getSubmachine() {
/**
* Gets the submachine.
*
* @return the submachine or null if not set
*/
public StateMachine<S, E> getSubmachine() {
return submachine;
}
protected Collection<Region<S, E>> getRegions() {
/**
* Gets the regions.
*
* @return the regions or empty collection if no regions
*/
public Collection<Region<S, E>> getRegions() {
return regions;
}

View File

@@ -121,7 +121,12 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
@Override
public void exit(E event, StateContext<S, E> context) {
getSubmachine().getState().exit(event, context);
getSubmachine().stop();
// don't stop if it looks like we're coming back
// stop would cause start with entry which would
// enable default transition and state
if (context.getTransition().getSource().getId() != getSubmachine().getState().getId()) {
getSubmachine().stop();
}
Collection<? extends Action<S, E>> actions = getExitActions();
if (actions != null && !isLocal(context)) {
for (Action<S, E> action : actions) {

View File

@@ -46,6 +46,7 @@ import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.processor.StateMachineHandler;
import org.springframework.statemachine.processor.StateMachineOnTransitionHandler;
import org.springframework.statemachine.processor.StateMachineRuntime;
import org.springframework.statemachine.state.AbstractState;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
@@ -279,8 +280,7 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
callHandlers(currentState, state, event);
currentState = state;
entryToState(state, event, transition);
setCurrentState(state, event, transition);
// TODO: should handle triggerles transition some how differently
for (Transition<S,E> t : transitions) {
@@ -294,6 +294,21 @@ public abstract class AbstractStateMachine<S, E> extends LifecycleObjectSupport
}
void setCurrentState(State<S, E> state, Message<E> event, Transition<S, E> transition) {
if (states.contains(state)) {
currentState = state;
entryToState(state, event, transition);
} else if (currentState.isSubmachineState()) {
if (transition != null && transition.getKind() == TransitionKind.EXTERNAL) {
entryToState(currentState, event, transition);
}
// TODO: should find a better way to trick setting state for submachine
// without a need to access package protected method via casting
StateMachine<S, E> submachine = ((AbstractState<S, E>)currentState).getSubmachine();
((AbstractStateMachine<S, E>)submachine).setCurrentState(state, event, transition);
}
}
private void exitFromState(State<S, E> state, Message<E> event, Transition<S, E> transition) {
if (state != null) {
log.trace("Exit state=[" + state + "]");

View File

@@ -73,6 +73,15 @@ public abstract class AbstractStateMachineTests {
E1,E2,E3,E4,EF
}
public static enum TestStates2 {
BUSY, PLAYING, PAUSED,
IDLE, CLOSED, OPEN
}
public static enum TestEvents2 {
PLAY, STOP, PAUSE, EJECT, LOAD
}
@Configuration
public static class BaseConfig {

View File

@@ -152,7 +152,7 @@ public class SubStateMachineTests extends AbstractStateMachineTests {
assertThat(exitActionS1.stateContexts.size(), is(1));
}
// @Test
@Test
public void testExternalTransition2() throws Exception {
/**
@@ -190,7 +190,7 @@ public class SubStateMachineTests extends AbstractStateMachineTests {
Collection<Action<TestStates, TestEvents>> entryActionsS112 = new ArrayList<Action<TestStates, TestEvents>>();
entryActionsS112.add(entryActionS112);
Collection<Action<TestStates, TestEvents>> exitActionsS112 = new ArrayList<Action<TestStates, TestEvents>>();
exitActionsS111.add(exitActionS112);
exitActionsS112.add(exitActionS112);
State<TestStates,TestEvents> stateS112 = new EnumState<TestStates,TestEvents>(TestStates.S112, null, entryActionsS112, exitActionsS112, null);
// submachine 1
@@ -231,14 +231,14 @@ public class SubStateMachineTests extends AbstractStateMachineTests {
assertThat(entryActionS112.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
assertThat(exitActionS112.onExecuteLatch.await(1, TimeUnit.SECONDS), is(false));
assertThat(entryActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
assertThat(exitActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(false));
assertThat(exitActionS1.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true));
assertThat(entryActionS111.stateContexts.size(), is(1));
assertThat(exitActionS111.stateContexts.size(), is(1));
assertThat(entryActionS112.stateContexts.size(), is(1));
assertThat(exitActionS112.stateContexts.size(), is(0));
assertThat(entryActionS1.stateContexts.size(), is(1));
assertThat(exitActionS1.stateContexts.size(), is(0));
assertThat(entryActionS1.stateContexts.size(), is(2));
assertThat(exitActionS1.stateContexts.size(), is(1));
}
@@ -389,6 +389,21 @@ public class SubStateMachineTests extends AbstractStateMachineTests {
assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S10));
}
@Test
public void testStateChangeWithinMachine() {
context.register(BaseConfig.class, StateMachineEventPublisherConfiguration.class, Config3.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
@SuppressWarnings("unchecked")
EnumStateMachine<TestStates2,TestEvents2> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class);
machine.start();
assertThat(machine, notNullValue());
assertThat(machine.getState().getIds(), contains(TestStates2.IDLE, TestStates2.CLOSED));
machine.sendEvent(TestEvents2.EJECT);
assertThat(machine.getState().getIds(), contains(TestStates2.IDLE, TestStates2.OPEN));
}
@Configuration
@EnableStateMachine
public static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@@ -473,4 +488,48 @@ public class SubStateMachineTests extends AbstractStateMachineTests {
}
@Configuration
@EnableStateMachine
static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates2, TestEvents2> {
@Override
public void configure(StateMachineStateConfigurer<TestStates2, TestEvents2> states) throws Exception {
states
.withStates()
.initial(TestStates2.IDLE)
.state(TestStates2.IDLE)
.and()
.withStates()
.parent(TestStates2.IDLE)
.initial(TestStates2.CLOSED)
.state(TestStates2.CLOSED)
.state(TestStates2.OPEN)
.and()
.withStates()
.state(TestStates2.BUSY)
.and()
.withStates()
.parent(TestStates2.BUSY)
.initial(TestStates2.PLAYING)
.state(TestStates2.PLAYING)
.state(TestStates2.PAUSED);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates2, TestEvents2> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates2.CLOSED)
.target(TestStates2.OPEN)
.event(TestEvents2.EJECT)
.and()
.withExternal()
.source(TestStates2.OPEN)
.target(TestStates2.CLOSED)
.event(TestEvents2.EJECT);
}
}
}