diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index 90710b66..f2922372 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -36,6 +37,7 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.context.Lifecycle; import org.springframework.core.OrderComparator; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; @@ -100,6 +102,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo private volatile Runnable task; + private final AtomicBoolean requestTask = new AtomicBoolean(false); + private final Map> handlers = new HashMap>(); private volatile boolean handlersInitialized; @@ -108,6 +112,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo private final Map, Transition> triggerToTransitionMap = new HashMap, Transition>(); + private final List> triggerlessTransitions = new ArrayList>(); + /** * Instantiates a new abstract state machine. * @@ -199,6 +205,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo if (trigger != null) { // we have same triggers with different transitions triggerToTransitionMap.put(trigger, transition); + } else { + triggerlessTransitions.add(transition); } } for (State state : states) { @@ -342,14 +350,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo setCurrentState(state, message, transition, true, stateMachine); } - // TODO: should handle triggerles transition some how differently - for (Transition t : transitions) { - State source = t.getSource(); - State target = t.getTarget(); - if (t.getTrigger() == null && source.equals(currentState)) { - switchToState(target, message, t, stateMachine); - } - } + scheduleEventQueueProcessing(); if (isComplete()) { stop(); } @@ -605,6 +606,10 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } private void scheduleEventQueueProcessing() { + TaskExecutor executor = getTaskExecutor(); + if (executor == null) { + return; + } if (task == null) { task = new Runnable() { @Override @@ -615,9 +620,14 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo processTriggerQueue(); } task = null; + if (requestTask.getAndSet(false)) { + scheduleEventQueueProcessing(); + } } }; - getTaskExecutor().execute(task); + executor.execute(task); + } else { + requestTask.set(true); } } @@ -661,34 +671,42 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } // go through candidates and transit max one - for (Transition t : trans) { - StateContext stateContext = buildStateContext(queuedMessage, t, this); - if (t == null) { - continue; - } - State source = t.getSource(); - if (source == null) { - continue; - } - if (!StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) { - continue; - } - boolean transit = t.transit(stateContext); - if (transit) { - // TODO: should change trasition api so that we can ask - // if transition will transit so that we can post - // accurate notifyTransitionStart - notifyTransitionStart(t); - callHandlers(t.getSource(), t.getTarget(), queuedMessage); - if (t.getKind() != TransitionKind.INTERNAL) { - switchToState(t.getTarget(), queuedMessage, t, this); - } - notifyTransition(t); - notifyTransitionEnd(t); - break; - } + handleTriggerTrans(trans, queuedMessage); + } + if (currentState != null) { + // handle triggerless transitions + handleTriggerTrans(triggerlessTransitions, null); + } + } + private void handleTriggerTrans(List> trans, Message queuedMessage) { + for (Transition t : trans) { + StateContext stateContext = buildStateContext(queuedMessage, t, this); + if (t == null) { + continue; } + State source = t.getSource(); + if (source == null) { + continue; + } + if (!StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) { + continue; + } + boolean transit = t.transit(stateContext); + if (transit) { + // TODO: should change trasition api so that we can ask + // if transition will transit so that we can post + // accurate notifyTransitionStart + notifyTransitionStart(t); + callHandlers(t.getSource(), t.getTarget(), queuedMessage); + if (t.getKind() != TransitionKind.INTERNAL) { + switchToState(t.getTarget(), queuedMessage, t, this); + } + notifyTransition(t); + notifyTransitionEnd(t); + break; + } + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java index eea9f32b..64b642c3 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java @@ -79,6 +79,12 @@ public abstract class AbstractStateMachineTests { IDLE, CLOSED, OPEN } + public static enum TestStates3 { + READY, + FORK, JOIN, + TASKS, T1, T1E, T2, T2E, T3, T3E + } + public static enum TestEvents2 { PLAY, STOP, PAUSE, EJECT, LOAD } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java index 3cf48333..a654fa0f 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java @@ -15,6 +15,7 @@ */ package org.springframework.statemachine.state; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; @@ -66,6 +67,33 @@ public class EndStateTests extends AbstractStateMachineTests { assertThat(machine.isComplete(), is(true)); } + @Test + public void testEndStatesWithRegions() throws InterruptedException { + context.register(Config2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + @SuppressWarnings("unchecked") + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + machine.start(); + machine.sendEvent(TestEvents.E1); + assertThat(machine.getState().getIds(), contains(TestStates3.READY)); + } + + // disable for now, fix in other ticket + //@Test + public void testEndStatesWithRegionsDefinedInStates() throws InterruptedException { + context.register(Config3.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + @SuppressWarnings("unchecked") + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + machine.start(); + machine.sendEvent(TestEvents.E1); + assertThat(machine.getState().getIds(), contains(TestStates3.READY)); + } + @Configuration @EnableStateMachine static class Config1 extends EnumStateMachineConfigurerAdapter { @@ -115,4 +143,143 @@ public class EndStateTests extends AbstractStateMachineTests { } + @Configuration + @EnableStateMachine + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(TestStates3.READY) + .fork(TestStates3.FORK) + .state(TestStates3.TASKS) + .join(TestStates3.JOIN) + .and() + .withStates() + .parent(TestStates3.TASKS) + .initial(TestStates3.T1) + .state(TestStates3.T1E) + .end(TestStates3.T1E) + .and() + .withStates() + .parent(TestStates3.TASKS) + .initial(TestStates3.T2) + .state(TestStates3.T2E) + .end(TestStates3.T2E) + .and() + .withStates() + .parent(TestStates3.TASKS) + .initial(TestStates3.T3) + .state(TestStates3.T3E) + .end(TestStates3.T3E); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(TestStates3.READY).target(TestStates3.FORK) + .event(TestEvents.E1) + .and() + .withFork() + .source(TestStates3.FORK) + .target(TestStates3.T1) + .target(TestStates3.T2) + .target(TestStates3.T3) + .and() + .withExternal() + .source(TestStates3.T1).target(TestStates3.T1E) + .and() + .withExternal() + .source(TestStates3.T2).target(TestStates3.T2E) + .and() + .withExternal() + .source(TestStates3.T3).target(TestStates3.T3E) + .and() + .withJoin() + .source(TestStates3.T1E) + .source(TestStates3.T2E) + .source(TestStates3.T3E) + .target(TestStates3.JOIN) + .and() + .withExternal() + .source(TestStates3.JOIN).target(TestStates3.READY); + } + + } + + @Configuration + @EnableStateMachine + static class Config3 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(TestStates3.READY) + .fork(TestStates3.FORK) + .state(TestStates3.TASKS) + .join(TestStates3.JOIN) + .and() + .withStates() + .parent(TestStates3.TASKS) + .initial(TestStates3.T1) + .state(TestStates3.T1E) + .end(TestStates3.T1E) + .and() + .withStates() + .parent(TestStates3.TASKS) + .initial(TestStates3.T2) + .state(TestStates3.T2E) + .end(TestStates3.T2E) + .and() + .withStates() + .parent(TestStates3.TASKS) + .initial(TestStates3.T3) + .state(TestStates3.T3E) + .end(TestStates3.T3E); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(TestStates3.READY).target(TestStates3.FORK) + .event(TestEvents.E1) + .and() + .withFork() + .source(TestStates3.FORK) + .target(TestStates3.T1) + .target(TestStates3.T2) + .target(TestStates3.T3) + .and() + .withExternal() + .state(TestStates3.TASKS) + .source(TestStates3.T1).target(TestStates3.T1E) + .and() + .withExternal() + .state(TestStates3.TASKS) + .source(TestStates3.T2).target(TestStates3.T2E) + .and() + .withExternal() + .state(TestStates3.TASKS) + .source(TestStates3.T3).target(TestStates3.T3E) + .and() + .withJoin() + .source(TestStates3.T1E) + .source(TestStates3.T2E) + .source(TestStates3.T3E) + .target(TestStates3.JOIN) + .and() + .withExternal() + .source(TestStates3.JOIN).target(TestStates3.READY); + } + + } + } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java index 5fece584..d9503d07 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java @@ -15,8 +15,9 @@ */ package org.springframework.statemachine.transition; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -24,6 +25,7 @@ import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Test; @@ -39,6 +41,8 @@ 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.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; /** * Tests for state machine transitions. @@ -48,73 +52,103 @@ import org.springframework.statemachine.config.builders.StateMachineTransitionCo */ public class TransitionTests extends AbstractStateMachineTests { + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + @SuppressWarnings({ "unchecked" }) @Test public void testTriggerlessTransition() throws Exception { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class, Config1.class); - assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + context.register(BaseConfig.class, Config1.class); + context.refresh(); + + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); EnumStateMachine machine = - ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S1)); + + listener.reset(2); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(2)); assertThat(machine.getState().getIds(), contains(TestStates.S3)); - ctx.close(); } @SuppressWarnings({ "unchecked" }) @Test public void testTriggerlessTransitionFromInitial() throws Exception { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class, Config3.class); - assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + context.register(BaseConfig.class, Config3.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); EnumStateMachine machine = - ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S2)); - ctx.close(); } @SuppressWarnings({ "unchecked" }) @Test public void testTriggerlessTransitionFromInitialToEnd() throws Exception { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class, Config4.class); - assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + context.register(BaseConfig.class, Config4.class); + context.refresh(); + + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); EnumStateMachine machine = - ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); machine.start(); // end state terminates sm so state is null assertThat(machine.getState(), nullValue()); assertThat(machine.isComplete(), is(true)); assertThat(machine.isRunning(), is(false)); - ctx.close(); + } + + @SuppressWarnings({ "unchecked" }) + @Test + public void testTriggerlessTransitionInRegionsDefinedInSubStates() throws Exception { + context.register(BaseConfig.class, Config5.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + machine.start(); + assertThat(machine.getState().getIds(), contains(TestStates.S1)); + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S201, TestStates.S211)); } @SuppressWarnings({ "unchecked" }) @Test public void testTriggerlessTransitionInRegions() throws Exception { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class, Config5.class); - assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + context.register(BaseConfig.class, Config6.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); EnumStateMachine machine = - ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); machine.start(); assertThat(machine.getState().getIds(), contains(TestStates.S1)); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); - assertThat(machine.getState().getIds(), contains(TestStates.S2)); - ctx.close(); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S201, TestStates.S211)); } @SuppressWarnings({ "unchecked" }) @Test public void testInternalTransition() throws Exception { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class, Config2.class); - assertTrue(ctx.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + context.register(BaseConfig.class, Config2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); EnumStateMachine machine = - ctx.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); machine.start(); - TestExitAction testExitAction = ctx.getBean("testExitAction", TestExitAction.class); - TestEntryAction testEntryAction = ctx.getBean("testEntryAction", TestEntryAction.class); - TestAction externalTestAction = ctx.getBean("externalTestAction", TestAction.class); - TestAction internalTestAction = ctx.getBean("internalTestAction", TestAction.class); + TestExitAction testExitAction = context.getBean("testExitAction", TestExitAction.class); + TestEntryAction testEntryAction = context.getBean("testEntryAction", TestEntryAction.class); + TestAction externalTestAction = context.getBean("externalTestAction", TestAction.class); + TestAction internalTestAction = context.getBean("internalTestAction", TestAction.class); assertThat(machine.getState().getIds(), contains(TestStates.S1)); assertThat(testExitAction.onExecuteLatch.await(1, TimeUnit.SECONDS), is(false)); @@ -131,7 +165,6 @@ public class TransitionTests extends AbstractStateMachineTests { assertThat(externalTestAction.onExecuteLatch.await(1, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(TestStates.S2)); - ctx.close(); } @Configuration @@ -275,13 +308,11 @@ public class TransitionTests extends AbstractStateMachineTests { .parent(TestStates.S2) .initial(TestStates.S20) .state(TestStates.S201) - .end(TestStates.S201) .and() .withStates() .parent(TestStates.S2) .initial(TestStates.S21) - .state(TestStates.S211) - .end(TestStates.S211); + .state(TestStates.S211); } @Override @@ -305,4 +336,63 @@ public class TransitionTests extends AbstractStateMachineTests { } + @Configuration + @EnableStateMachine + public static class Config6 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S20) + .state(TestStates.S201) + .and() + .withStates() + .parent(TestStates.S2) + .initial(TestStates.S21) + .state(TestStates.S211); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S20) + .target(TestStates.S201) + .and() + .withExternal() + .source(TestStates.S21) + .target(TestStates.S211); + } + + } + + static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile int stateChangedCount = 0; + + @Override + public void stateChanged(State from, State to) { + stateChangedLatch.countDown(); + stateChangedCount++; + } + + public void reset(int c1) { + stateChangedLatch = new CountDownLatch(c1); + stateChangedCount = 0; + } + + } + }