Add error actions to states

- Now do behaviour and entry/error actions
  can have error action callback with
  a raised Exception.
- New modified added methods in a StateConfigurer.
- Fixes #277
This commit is contained in:
Janne Valkealahti
2016-11-19 09:50:28 +00:00
parent f28f4567bb
commit 0dd9d2fa66
3 changed files with 266 additions and 13 deletions

View File

@@ -25,6 +25,7 @@ import java.util.UUID;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.action.Actions;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineStateBuilder;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
@@ -164,6 +165,21 @@ public class DefaultStateConfigurer<S, E>
return state(state, stateActions);
}
@Override
public StateConfigurer<S, E> stateDo(S state, Action<S, E> action) {
return stateDo(state, action, null);
}
@Override
public StateConfigurer<S, E> stateDo(S state, Action<S, E> action, Action<S, E> error) {
Collection<Action<S, E>> stateActions = null;
if (action != null) {
stateActions = new ArrayList<Action<S, E>>(1);
stateActions.add(error != null ? Actions.errorCallingAction(action, error) : action);
}
return state(state, stateActions);
}
@Override
public StateConfigurer<S, E> state(S state, Collection<? extends Action<S, E>> entryActions,
Collection<? extends Action<S, E>> exitActions) {
@@ -186,6 +202,36 @@ public class DefaultStateConfigurer<S, E>
return state(state, entryActions, exitActions);
}
@Override
public StateConfigurer<S, E> stateEntry(S state, Action<S, E> action) {
return state(state, action, null);
}
@Override
public StateConfigurer<S, E> stateEntry(S state, Action<S, E> action, Action<S, E> error) {
Collection<Action<S, E>> entryActions = null;
if (action != null) {
entryActions = new ArrayList<Action<S, E>>(1);
entryActions.add(error != null ? Actions.errorCallingAction(action, error) : action);
}
return state(state, entryActions, null);
}
@Override
public StateConfigurer<S, E> stateExit(S state, Action<S, E> action) {
return state(state, null, action);
}
@Override
public StateConfigurer<S, E> stateExit(S state, Action<S, E> action, Action<S, E> error) {
Collection<Action<S, E>> exitActions = null;
if (action != null) {
exitActions = new ArrayList<Action<S, E>>(1);
exitActions.add(error != null ? Actions.errorCallingAction(action, error) : action);
}
return state(state, null, exitActions);
}
@SuppressWarnings("unchecked")
@Override
public StateConfigurer<S, E> state(S state, E... deferred) {

View File

@@ -109,6 +109,27 @@ public interface StateConfigurer<S, E> extends
*/
StateConfigurer<S, E> state(S state, Action<S, E> stateAction);
/**
* Specify a state {@code S} with state behaviour {@link Action}.
* Currently synonym for {@link #state(Object, Action)}.
*
* @param state the state
* @param action the state action
* @return configurer for chaining
* @see #state(Object, Action)
*/
StateConfigurer<S, E> stateDo(S state, Action<S, E> action);
/**
* Specify a state {@code S} with state behaviour {@link Action} and
* error {@link Action} callback.
*
* @param state the state
* @param action the state action
* @return configurer for chaining
*/
StateConfigurer<S, E> stateDo(S state, Action<S, E> action, Action<S, E> error);
/**
* Specify a state {@code S} with entry and exit {@link Action}s.
*
@@ -130,6 +151,56 @@ public interface StateConfigurer<S, E> extends
*/
StateConfigurer<S, E> state(S state, Action<S, E> entryAction, Action<S, E> exitAction);
/**
* Specify a state {@code S} with state entry {@link Action}.
* Currently synonym for {@link #state(Object, Action, Action)}
* with no exit action.
*
* @param state the state
* @param action the state entry action
* @return configurer for chaining
* @see #state(Object, Action, Action)
*/
StateConfigurer<S, E> stateEntry(S state, Action<S, E> action);
/**
* Specify a state {@code S} with state entry {@link Action} and
* error {@link Action} callback.
* Currently synonym for {@link #state(Object, Action, Action)}
* with no exit action.
*
* @param state the state
* @param action the state entry action
* @return configurer for chaining
* @see #state(Object, Action, Action)
*/
StateConfigurer<S, E> stateEntry(S state, Action<S, E> action, Action<S, E> error);
/**
* Specify a state {@code S} with state exit {@link Action}.
* Currently synonym for {@link #state(Object, Action, Action)}
* with no entry action.
*
* @param state the state
* @param action the state exit action
* @return configurer for chaining
* @see #state(Object, Action, Action)
*/
StateConfigurer<S, E> stateExit(S state, Action<S, E> action);
/**
* Specify a state {@code S} with state exit {@link Action} and
* error {@link Action} callback.
* Currently synonym for {@link #state(Object, Action, Action)}
* with no entry action.
*
* @param state the state
* @param action the state entry action
* @return configurer for chaining
* @see #state(Object, Action, Action)
*/
StateConfigurer<S, E> stateExit(S state, Action<S, E> action, Action<S, E> error);
/**
* Specify a state {@code S} with a deferred events {@code E}.
*

View File

@@ -21,6 +21,9 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -48,35 +51,36 @@ public class ActionTests extends AbstractStateMachineTests {
@SuppressWarnings({ "unchecked" })
@Test
public void testTransitionActions() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config1.class);
assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
context.register(Config1.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<TestStates,TestEvents> machine =
ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
TestCountAction testAction1 = ctx.getBean("testAction1", TestCountAction.class);
TestCountAction testAction2 = ctx.getBean("testAction2", TestCountAction.class);
TestCountAction testAction3 = ctx.getBean("testAction3", TestCountAction.class);
TestCountAction testAction1 = context.getBean("testAction1", TestCountAction.class);
TestCountAction testAction2 = context.getBean("testAction2", TestCountAction.class);
TestCountAction testAction3 = context.getBean("testAction3", TestCountAction.class);
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E2).build());
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E3).build());
assertThat(testAction1.count, is(1));
assertThat(testAction2.count, is(1));
assertThat(testAction3.count, is(1));
ctx.close();
}
@SuppressWarnings({ "unchecked" })
@Test
public void testTransitionActionErrors() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config2.class);
assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
context.register(Config2.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<TestStates,TestEvents> machine =
ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
TestCountAction testAction1 = ctx.getBean("testAction1", TestCountAction.class);
TestCountAction testErrorAction = ctx.getBean("testErrorAction", TestCountAction.class);
TestCountAction testAction1 = context.getBean("testAction1", TestCountAction.class);
TestCountAction testErrorAction = context.getBean("testErrorAction", TestCountAction.class);
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(testAction1.count, is(1));
assertThat(testErrorAction.count, is(1));
@@ -84,7 +88,53 @@ public class ActionTests extends AbstractStateMachineTests {
assertThat(testErrorAction.context.getException(), notNullValue());
assertThat(testErrorAction.context.getException(), instanceOf(RuntimeException.class));
assertThat(testErrorAction.context.getException().getMessage(), is("Fake Error"));
ctx.close();
}
@SuppressWarnings({ "unchecked" })
@Test
public void testStateActionErrors() throws Exception {
context.register(Config3.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
TestCountAction testAction2 = context.getBean("testAction2", TestCountAction.class);
TestCountAction testAction3 = context.getBean("testAction3", TestCountAction.class);
TestCountAction testAction4 = context.getBean("testAction4", TestCountAction.class);
TestCountAction testErrorAction2 = context.getBean("testErrorAction2", TestCountAction.class);
TestCountAction testErrorAction3 = context.getBean("testErrorAction3", TestCountAction.class);
TestCountAction testErrorAction4 = context.getBean("testErrorAction4", TestCountAction.class);
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(machine.getState().getId(), is(TestStates.S2));
assertThat(testErrorAction3.latch.await(1, TimeUnit.SECONDS), is(true));
assertThat(testErrorAction2.latch.await(1, TimeUnit.SECONDS), is(true));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E2).build());
assertThat(testErrorAction4.latch.await(1, TimeUnit.SECONDS), is(true));
assertThat(testAction2.count, is(1));
assertThat(testErrorAction2.count, is(1));
assertThat(testErrorAction2.context, notNullValue());
assertThat(testErrorAction2.context.getException(), notNullValue());
assertThat(testErrorAction2.context.getException(), instanceOf(RuntimeException.class));
assertThat(testErrorAction2.context.getException().getMessage(), is("Fake Error"));
assertThat(testAction3.count, is(1));
assertThat(testErrorAction3.count, is(1));
assertThat(testErrorAction3.context, notNullValue());
assertThat(testErrorAction3.context.getException(), notNullValue());
assertThat(testErrorAction3.context.getException(), instanceOf(RuntimeException.class));
assertThat(testErrorAction3.context.getException().getMessage(), is("Fake Error"));
assertThat(testAction4.count, is(1));
assertThat(testErrorAction4.count, is(1));
assertThat(testErrorAction4.context, notNullValue());
assertThat(testErrorAction4.context.getException(), notNullValue());
assertThat(testErrorAction4.context.getException(), instanceOf(RuntimeException.class));
assertThat(testErrorAction4.context.getException().getMessage(), is("Fake Error"));
}
@Test
@@ -92,10 +142,16 @@ public class ActionTests extends AbstractStateMachineTests {
}
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
private static class TestCountAction implements Action<TestStates, TestEvents> {
int count = 0;
StateContext<TestStates, TestEvents> context;
CountDownLatch latch = new CountDownLatch(1);
public TestCountAction() {
count = 0;
@@ -105,6 +161,7 @@ public class ActionTests extends AbstractStateMachineTests {
public void execute(StateContext<TestStates, TestEvents> context) {
this.context = context;
count++;
latch.countDown();
}
}
@@ -212,4 +269,83 @@ public class ActionTests 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.S1)
.state(TestStates.S2)
.stateDo(TestStates.S2, testAction2(), testErrorAction2())
.stateEntry(TestStates.S2, testAction3(), testErrorAction3())
.stateExit(TestStates.S2, testAction4(), testErrorAction4())
.state(TestStates.S3);
}
@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.S2)
.target(TestStates.S3)
.event(TestEvents.E2);
}
@Bean
public TestCountAction testAction2() {
return new TestCountAction() {
@Override
public void execute(StateContext<TestStates, TestEvents> context) {
super.execute(context);
throw new RuntimeException("Fake Error");
}
};
}
@Bean
public TestCountAction testAction3() {
return new TestCountAction() {
@Override
public void execute(StateContext<TestStates, TestEvents> context) {
super.execute(context);
throw new RuntimeException("Fake Error");
}
};
}
@Bean
public TestCountAction testAction4() {
return new TestCountAction() {
@Override
public void execute(StateContext<TestStates, TestEvents> context) {
super.execute(context);
throw new RuntimeException("Fake Error");
}
};
}
@Bean
public TestCountAction testErrorAction2() {
return new TestCountAction();
}
@Bean
public TestCountAction testErrorAction3() {
return new TestCountAction();
}
@Bean
public TestCountAction testErrorAction4() {
return new TestCountAction();
}
}
}