diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java index 747fdfdd..11e45d0f 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java @@ -173,7 +173,8 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im interceptors.add(interceptor); } - private void handleTriggerTrans(List> trans, Message queuedMessage) { + private boolean handleTriggerTrans(List> trans, Message queuedMessage) { + boolean transit = false; for (Transition t : trans) { if (t == null) { continue; @@ -183,6 +184,9 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im continue; } State currentState = stateMachine.getState(); + if (currentState == null) { + continue; + } if (!StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) { continue; } @@ -193,13 +197,14 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im break; } - boolean transit = t.transit(stateContext); + transit = t.transit(stateContext); if (transit) { stateMachineExecutorTransit.transit(t, stateContext, queuedMessage); interceptors.postTransition(stateContext); break; } } + return transit; } private void handleInitialTrans(Transition tran, Message queuedMessage) { @@ -282,6 +287,9 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im } log.debug("Process trigger queue"); TriggerQueueItem queueItem = null; + // keep last message here so that we can + // pass it to triggerless transitions + Message queuedMessage = null; while ((queueItem = triggerQueue.poll()) != null) { State currentState = stateMachine.getState(); @@ -290,7 +298,7 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im continue; } - Message queuedMessage = queueItem.message; + queuedMessage = queueItem.message; E event = queuedMessage != null ? queuedMessage.getPayload() : null; // need all transitions trigger could match, event trigger may match @@ -326,8 +334,12 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im handleTriggerTrans(trans, queuedMessage); } if (stateMachine.getState() != null) { - // handle triggerless transitions - handleTriggerTrans(triggerlessTransitions, null); + // loop triggerless transitions here so that + // all "chained" transitions will get queue message + boolean transit = false; + do { + transit = handleTriggerTrans(triggerlessTransitions, queuedMessage); + } while (transit); } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionEventHeaderTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionEventHeaderTests.java new file mode 100644 index 00000000..0ce68163 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionEventHeaderTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.transition; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.ObjectStateMachine; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.transition.TransitionTests.TestListener; + +public class TransitionEventHeaderTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @SuppressWarnings("unchecked") + @Test + public void testEventPassedOnWithTrigggerless() throws Exception { + context.register(Config1.class); + context.refresh(); + + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + + EventCheckAction eventCheckAction1 = context.getBean("eventCheckAction1", EventCheckAction.class); + EventCheckAction eventCheckAction2 = context.getBean("eventCheckAction2", EventCheckAction.class); + EventCheckAction eventCheckAction3 = context.getBean("eventCheckAction3", EventCheckAction.class); + EventCheckAction eventCheckAction4 = context.getBean("eventCheckAction4", EventCheckAction.class); + + TestListener listener = new TestListener(); + machine.addStateListener(listener); + + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + listener.reset(3); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(3)); + assertThat(machine.getState().getIds(), contains(TestStates.S4)); + + assertThat(eventCheckAction1.context.getEvent(), nullValue()); + assertThat(eventCheckAction2.context.getEvent(), is(TestEvents.E1)); + assertThat(eventCheckAction3.context.getEvent(), is(TestEvents.E1)); + assertThat(eventCheckAction4.context.getEvent(), is(TestEvents.E1)); + } + + @Configuration + @EnableStateMachine + public static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1, eventCheckAction1(), null) + .state(TestStates.S2, eventCheckAction2(), null) + .state(TestStates.S3, eventCheckAction3(), null) + .state(TestStates.S4, eventCheckAction4(), null); + } + + @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) + .and() + .withExternal() + .source(TestStates.S3) + .target(TestStates.S4); + } + + @Bean + public EventCheckAction eventCheckAction1() { + return new EventCheckAction(); + } + + @Bean + public EventCheckAction eventCheckAction2() { + return new EventCheckAction(); + } + + @Bean + public EventCheckAction eventCheckAction3() { + return new EventCheckAction(); + } + + @Bean + public EventCheckAction eventCheckAction4() { + return new EventCheckAction(); + } + } + + private static class EventCheckAction implements Action { + + StateContext context; + + @Override + public void execute(StateContext context) { + this.context = context; + } + + } + +}