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:
@@ -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) {
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user