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:
Janne Valkealahti
2016-10-29 08:48:53 +01:00
parent 728dd433d9
commit bd7e0141ea
4 changed files with 213 additions and 1 deletions

View File

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

View File

@@ -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();

View File

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

View File

@@ -19,6 +19,8 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -71,13 +73,36 @@ public class PersistStateMachineHandlerTests {
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S1"));
}
@Test
public void testChoice() throws Exception {
StateMachine<String,String> stateMachine = buildTestStateMachine2();
PersistStateMachineHandler handler = new PersistStateMachineHandler(stateMachine);
handler.afterPropertiesSet();
handler.start();
TestPersistStateChangeListener listener = new TestPersistStateChangeListener();
handler.addPersistStateChangeListener(listener);
Message<String> event = MessageBuilder.withPayload("E1").build();
boolean accepted = handler.handleEventWithState(event, "SI");
assertThat(accepted, is(true));
assertThat(listener.latch.await(1, TimeUnit.SECONDS), is(true));
assertThat(listener.states.size(), is(1));
assertThat(listener.states.get(0).getId(), is("S2"));
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2"));
}
private static class TestPersistStateChangeListener implements PersistStateChangeListener {
CountDownLatch latch = new CountDownLatch(1);
List<State<String, String>> states = new ArrayList<>();
@Override
public void onPersist(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
states.add(state);
latch.countDown();
}
@@ -108,4 +133,30 @@ public class PersistStateMachineHandlerTests {
return builder.build();
}
private static StateMachine<String, String> buildTestStateMachine2()
throws Exception {
StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();
builder.configureConfiguration()
.withConfiguration()
.taskExecutor(new SyncTaskExecutor())
.autoStartup(true);
builder.configureStates()
.withStates()
.initial("SI")
.choice("S1")
.state("S2")
.state("S3");
builder.configureTransitions()
.withExternal()
.source("SI").target("S1").event("E1")
.and()
.withChoice()
.source("S1")
.last("S2");
return builder.build();
}
}