Fix interceptor handling
- Modify so that interceptor `preStateChange` is not called for transient states which would falsely try to i.e. persist choice and other states with persist recipe. - Added some tests to verify that `preStateChange` is called only once. - Relates to #266
This commit is contained in:
@@ -766,7 +766,8 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
}
|
||||
|
||||
private void switchToState(State<S,E> state, Message<E> message, Transition<S,E> transition, StateMachine<S, E> stateMachine) {
|
||||
if (!isInitialTransition(transition) && !callPreStateChangeInterceptors(state, message, transition, stateMachine)) {
|
||||
if (!isInitialTransition(transition) && !StateMachineUtils.isTransientPseudoState(state)
|
||||
&& !callPreStateChangeInterceptors(state, message, transition, stateMachine)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,35 @@ public abstract class StateMachineUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if state is a transient pseudo state, meaning it is a pseudostate
|
||||
* and its kind indicates that machine will never stay on this state after
|
||||
* run to completion has finished.
|
||||
*
|
||||
* @param <S> the type of state
|
||||
* @param <E> the type of event
|
||||
* @param state the state
|
||||
* @return true, if is normal pseudo state
|
||||
*/
|
||||
public static <S, E> boolean isTransientPseudoState(State<S, E> state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
PseudoState<S, E> pseudoState = state.getPseudoState();
|
||||
if (pseudoState == null) {
|
||||
return false;
|
||||
}
|
||||
PseudoStateKind kind = pseudoState.getKind();
|
||||
if (kind == PseudoStateKind.CHOICE || kind == PseudoStateKind.JUNCTION || kind == PseudoStateKind.ENTRY
|
||||
|| kind == PseudoStateKind.EXIT || kind == PseudoStateKind.HISTORY_DEEP
|
||||
|| kind == PseudoStateKind.HISTORY_SHALLOW || kind == PseudoStateKind.FORK
|
||||
|| kind == PseudoStateKind.JOIN) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static <S, E> boolean isPseudoState(State<S, E> state, PseudoStateKind kind) {
|
||||
if (state != null) {
|
||||
PseudoState<S, E> pseudoState = state.getPseudoState();
|
||||
|
||||
@@ -89,6 +89,81 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests {
|
||||
assertThat((Integer)machine.getExtendedState().getVariables().get("foo"), is(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntercept2() throws InterruptedException {
|
||||
context.register(Config2.class);
|
||||
context.refresh();
|
||||
@SuppressWarnings("unchecked")
|
||||
StateMachine<States, Events> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
|
||||
TestListener listener = new TestListener();
|
||||
machine.addStateListener(listener);
|
||||
TestStateChangeInterceptor interceptor = new TestStateChangeInterceptor();
|
||||
|
||||
machine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<States, Events>>() {
|
||||
|
||||
@Override
|
||||
public void apply(StateMachineAccess<States, Events> function) {
|
||||
function.addStateMachineInterceptor(interceptor);
|
||||
}
|
||||
});
|
||||
|
||||
machine.start();
|
||||
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(1));
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
|
||||
|
||||
interceptor.reset(1);
|
||||
listener.reset(1);
|
||||
machine.sendEvent(Events.A);
|
||||
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(1));
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S1));
|
||||
assertThat(interceptor.preStateChangeLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(interceptor.preStateChangeCount, is(1));
|
||||
|
||||
interceptor.reset(1);
|
||||
listener.reset(1);
|
||||
machine.sendEvent(Events.B);
|
||||
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(1));
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S2));
|
||||
assertThat(interceptor.preStateChangeLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(interceptor.preStateChangeCount, is(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntercept3() throws InterruptedException {
|
||||
context.register(Config3.class);
|
||||
context.refresh();
|
||||
@SuppressWarnings("unchecked")
|
||||
StateMachine<States, Events> machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
|
||||
TestListener listener = new TestListener();
|
||||
machine.addStateListener(listener);
|
||||
TestStateChangeInterceptor interceptor = new TestStateChangeInterceptor();
|
||||
|
||||
machine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<States, Events>>() {
|
||||
|
||||
@Override
|
||||
public void apply(StateMachineAccess<States, Events> function) {
|
||||
function.addStateMachineInterceptor(interceptor);
|
||||
}
|
||||
});
|
||||
|
||||
machine.start();
|
||||
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(1));
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0));
|
||||
|
||||
interceptor.reset(1);
|
||||
listener.reset(1);
|
||||
machine.sendEvent(Events.A);
|
||||
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(1));
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(States.S2));
|
||||
assertThat(interceptor.preStateChangeLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(interceptor.preStateChangeCount, is(1));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config1 extends EnumStateMachineConfigurerAdapter<States, Events> {
|
||||
@@ -210,6 +285,62 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config2 extends EnumStateMachineConfigurerAdapter<States, Events> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<States, Events> states)
|
||||
throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(States.S0)
|
||||
.state(States.S1)
|
||||
.state(States.S2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
|
||||
throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(States.S0).target(States.S1)
|
||||
.event(Events.A)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(States.S1).target(States.S2)
|
||||
.event(Events.B);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config3 extends EnumStateMachineConfigurerAdapter<States, Events> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<States, Events> states)
|
||||
throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(States.S0)
|
||||
.choice(States.S1)
|
||||
.state(States.S2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
|
||||
throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(States.S0).target(States.S1)
|
||||
.event(Events.A)
|
||||
.and()
|
||||
.withChoice()
|
||||
.source(States.S1)
|
||||
.last(States.S2);
|
||||
}
|
||||
}
|
||||
|
||||
public static enum States {
|
||||
S0, S1, S11, S12, S2, S21, S211, S212
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user