Add support for linked choice states

- With linked choices a machine got stuck in a pseudostate
  while it should automatically follow the links till
  non-pseudostate reached.
- Add ChoicePseudoState to use holder as state references
  may not be known at a time of its instantiation.
- Fixes #195
This commit is contained in:
Janne Valkealahti
2016-04-16 09:50:26 +01:00
parent 4e7b3eb66b
commit 3c7edd433e
4 changed files with 87 additions and 10 deletions

View File

@@ -489,7 +489,11 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
List<ChoiceData<S, E>> list = stateMachineTransitions.getChoices().get(s);
List<ChoiceStateData<S, E>> choices = new ArrayList<ChoiceStateData<S, E>>();
for (ChoiceData<S, E> c : list) {
choices.add(new ChoiceStateData<S, E>(stateMap.get(c.getTarget()), c.getGuard()));
StateHolder<S, E> holder = new StateHolder<S, E>(stateMap.get(c.getTarget()));
if (holder.getState() == null) {
holderMap.put(c.getTarget(), holder);
}
choices.add(new ChoiceStateData<S, E>(holder, c.getGuard()));
}
PseudoState<S, E> pseudoState = new ChoicePseudoState<S, E>(choices);
state = buildStateInternal(stateData.getState(), stateData.getDeferred(), stateData.getEntryActions(),

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import java.util.List;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.guard.Guard;
import org.springframework.util.Assert;
/**
* Choice implementation of a {@link PseudoState}.
@@ -74,27 +75,37 @@ public class ChoicePseudoState<S, E> implements PseudoState<S, E> {
* @param <E> the type of event
*/
public static class ChoiceStateData<S, E> {
private final State<S, E> state;
private final StateHolder<S, E> state;
private final Guard<S, E> guard;
/**
* Instantiates a new choice state data.
*
* @param state the state
* @param state the state holder
* @param guard the guard
*/
public ChoiceStateData(State<S, E> state, Guard<S, E> guard) {
public ChoiceStateData(StateHolder<S, E> state, Guard<S, E> guard) {
Assert.notNull(state, "Holder must be set");
this.state = state;
this.guard = guard;
}
/**
* Gets the state holder.
*
* @return the state holder
*/
public StateHolder<S, E> getStateHolder() {
return state;
}
/**
* Gets the state.
*
* @return the state
*/
public State<S, E> getState() {
return state;
return state.getState();
}
/**
@@ -106,5 +117,4 @@ public class ChoicePseudoState<S, E> implements PseudoState<S, E> {
return guard;
}
}
}

View File

@@ -729,6 +729,13 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
StateContext<S, E> stateContext = buildStateContext(Stage.STATE_CHANGED, message, transition, stateMachine);
State<S, E> toState = state.getPseudoState().entry(stateContext);
// TODO: should do recursive linked pseudostates centrally
if (kind == PseudoStateKind.CHOICE) {
while (toState != null && toState.getPseudoState() != null
&& toState.getPseudoState().getKind() != PseudoStateKind.INITIAL) {
toState = toState.getPseudoState().entry(stateContext);
}
}
if (kind == PseudoStateKind.CHOICE) {
callPreStateChangeInterceptors(toState, message, transition, stateMachine);
}

View File

@@ -114,6 +114,20 @@ public class ChoiceStateTests extends AbstractStateMachineTests {
assertThat(machine.getState().getIds(), contains(TestStates.S33));
}
@Test
@SuppressWarnings("unchecked")
public void testSubsequentChoiceStates() {
context.register(BaseConfig.class, Config3.class);
context.refresh();
ObjectStateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
assertThat(machine, notNullValue());
machine.start();
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).setHeader("choice", "s2").build());
assertThat(machine.getState().getIds(), contains(TestStates.S21));
}
@Configuration
@EnableStateMachine
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@@ -160,6 +174,7 @@ public class ChoiceStateTests extends AbstractStateMachineTests {
}
}
@Configuration
@EnableStateMachine
static class Config2 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@@ -184,9 +199,6 @@ public class ChoiceStateTests extends AbstractStateMachineTests {
.and()
.withChoice()
.source(TestStates.S3)
// .first(TestStates.S30, s30Guard())
// .then(TestStates.S31, s31Guard())
// .then(TestStates.S32, s32Guard())
.last(TestStates.S33);
}
@@ -207,6 +219,50 @@ public class ChoiceStateTests extends AbstractStateMachineTests {
}
@Configuration
@EnableStateMachine
static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.SI)
.states(EnumSet.allOf(TestStates.class))
.choice(TestStates.S3)
.choice(TestStates.S2)
.end(TestStates.SF);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.SI)
.target(TestStates.S3)
.event(TestEvents.E1)
.and()
.withChoice()
.source(TestStates.S3)
.first(TestStates.S2, s2Guard())
.last(TestStates.S33)
.and()
.withChoice()
.source(TestStates.S2)
.first(TestStates.S20, s20Guard())
.last(TestStates.S21);
}
@Bean
public Guard<TestStates, TestEvents> s2Guard() {
return new ChoiceGuard("s2");
}
@Bean
public Guard<TestStates, TestEvents> s20Guard() {
return new ChoiceGuard("s20");
}
}
private static class ChoiceGuard implements Guard<TestStates, TestEvents> {