diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java index 4ea72cd2..b9eb883d 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java @@ -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 return state(state, stateActions); } + @Override + public StateConfigurer stateDo(S state, Action action) { + return stateDo(state, action, null); + } + + @Override + public StateConfigurer stateDo(S state, Action action, Action error) { + Collection> stateActions = null; + if (action != null) { + stateActions = new ArrayList>(1); + stateActions.add(error != null ? Actions.errorCallingAction(action, error) : action); + } + return state(state, stateActions); + } + @Override public StateConfigurer state(S state, Collection> entryActions, Collection> exitActions) { @@ -186,6 +202,36 @@ public class DefaultStateConfigurer return state(state, entryActions, exitActions); } + @Override + public StateConfigurer stateEntry(S state, Action action) { + return state(state, action, null); + } + + @Override + public StateConfigurer stateEntry(S state, Action action, Action error) { + Collection> entryActions = null; + if (action != null) { + entryActions = new ArrayList>(1); + entryActions.add(error != null ? Actions.errorCallingAction(action, error) : action); + } + return state(state, entryActions, null); + } + + @Override + public StateConfigurer stateExit(S state, Action action) { + return state(state, null, action); + } + + @Override + public StateConfigurer stateExit(S state, Action action, Action error) { + Collection> exitActions = null; + if (action != null) { + exitActions = new ArrayList>(1); + exitActions.add(error != null ? Actions.errorCallingAction(action, error) : action); + } + return state(state, null, exitActions); + } + @SuppressWarnings("unchecked") @Override public StateConfigurer state(S state, E... deferred) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java index 61d0e0a0..71168b27 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java @@ -109,6 +109,27 @@ public interface StateConfigurer extends */ StateConfigurer state(S state, Action 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 stateDo(S state, Action 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 stateDo(S state, Action action, Action error); + /** * Specify a state {@code S} with entry and exit {@link Action}s. * @@ -130,6 +151,56 @@ public interface StateConfigurer extends */ StateConfigurer state(S state, Action entryAction, Action 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 stateEntry(S state, Action 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 stateEntry(S state, Action action, Action 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 stateExit(S state, Action 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 stateExit(S state, Action action, Action error); + /** * Specify a state {@code S} with a deferred events {@code E}. * diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/ActionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/ActionTests.java index 04e9bcf5..f4e659bf 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/ActionTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/ActionTests.java @@ -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 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 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 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 { int count = 0; StateContext context; + CountDownLatch latch = new CountDownLatch(1); public TestCountAction() { count = 0; @@ -105,6 +161,7 @@ public class ActionTests extends AbstractStateMachineTests { public void execute(StateContext context) { this.context = context; count++; + latch.countDown(); } } @@ -212,4 +269,83 @@ public class ActionTests extends AbstractStateMachineTests { } } + + @Configuration + @EnableStateMachine + static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer 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 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 context) { + super.execute(context); + throw new RuntimeException("Fake Error"); + } + }; + } + + @Bean + public TestCountAction testAction3() { + return new TestCountAction() { + @Override + public void execute(StateContext context) { + super.execute(context); + throw new RuntimeException("Fake Error"); + } + }; + } + + @Bean + public TestCountAction testAction4() { + return new TestCountAction() { + @Override + public void execute(StateContext 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(); + } + } }