diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index f28421ad..ae212679 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -115,13 +115,51 @@ transition between states `S2` and `S3`. Both guard in above sample always evaluate to true. === Configuring Actions -Actions can be defined with various steps within a state transitions. +Actions can be defined to be executed with transitions and states +itself. Action is always executed as a result of a transition which +originates from a trigger. [source,java,indent=0] ---- -include::samples/DocsConfigurationSampleTests.java[tags=snippetE] +include::samples/DocsConfigurationSampleTests.java[tags=snippetEA] ---- +In above a single `Action` is defined as bean `action` and associated +with a transition from `S1` to `S2`. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetEB] +---- + +[NOTE] +==== +Usually you would not define same `Action` instance for different +stages but we did it here not to make too much noise in a code +snippet. +==== + +In above a single `Action` is defined as bean `action` and associated +with states `S1`, `S2` and `S3`. There is more going on there which +needs more clarification: + +* We defined action for initial state `S1`. +* We defined entry action for state `S1` and left exit action empty. +* We defined exit action for state `S2` and left entry action empty. +* We defined entry action as well as exit action for state `S3`. +* Notice how state `S1` is used twice with `initial()` and `state()` + functions. This is only needed if you want to define entry or exit + actions with initial state. + +[IMPORTANT] +==== +Defining action with `initial()` function only executes particular +action when state machine or sub state is started. Think this action +to be initializing action which is only executed once. Action defined +with `state()` is then executed if state machine is transitioning back +and forward between initial and non-inital states. +==== + === Configuring Pseudo States _Pseudo state_ configuration is usually done by configuring states and diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java index acd39292..4f7fe938 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java @@ -15,6 +15,7 @@ */ package org.springframework.statemachine.access; +import org.springframework.messaging.Message; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.support.StateMachineInterceptor; @@ -58,4 +59,13 @@ public interface StateMachineAccess { */ void setInitialEnabled(boolean enabled); + /** + * Set initial forwarded event which is used for passing in + * event and its headers for actions executed when sub state + * is entered via initial transition. + * + * @param message the forwarded message + */ + void setForwardedInitialEvent(Message message); + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java index 1441852c..49fa9a0d 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.access.StateMachineAccess; @@ -154,7 +155,7 @@ public class StateMachineState extends AbstractState { } @Override - public void entry(StateContext context) { + public void entry(final StateContext context) { Collection> actions = getEntryActions(); if (actions != null && !isLocal(context)) { for (Action action : actions) { @@ -166,8 +167,20 @@ public class StateMachineState extends AbstractState { State target = context.getTransition().getTarget(); State immediateDeepParent = findDeepParent(getSubmachine().getStates(), target); + if (context.getEvent() != null) { + getSubmachine().getStateMachineAccessor().doWithRegion( + new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess function) { + function.setForwardedInitialEvent(MessageBuilder.withPayload(context.getEvent()) + .copyHeaders(context.getMessageHeaders()).build()); + } + }); + } + // disable initial state where needed - if (immediateDeepParent != null && immediateDeepParent.isSubmachineState() && ( !isInitial(target) ) ) { + if (immediateDeepParent != null && immediateDeepParent.isSubmachineState() && (!isInitial(target))) { ((StateMachineState) immediateDeepParent).getSubmachine().getStateMachineAccessor() .doWithRegion(new StateMachineFunction>() { @@ -189,8 +202,8 @@ public class StateMachineState extends AbstractState { } }); } else if (immediateDeepParent != null && isInitial(immediateDeepParent) && isInitial(target)) { - ((StateMachineState) immediateDeepParent).getSubmachine().getStateMachineAccessor().doWithRegion( - new StateMachineFunction>() { + ((StateMachineState) immediateDeepParent).getSubmachine().getStateMachineAccessor() + .doWithRegion(new StateMachineFunction>() { @Override public void apply(StateMachineAccess function) { 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 4ea91039..a077c0db 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 @@ -113,6 +113,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo private String id = UUID.randomUUID().toString(); + private volatile Message forwardedInitialEvent; + /** * Instantiates a new abstract state machine. * @@ -294,6 +296,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo log.debug("Initial disable asked, disabling initial"); } stateMachineExecutor.setInitialEnabled(false); + } else { + stateMachineExecutor.setForwardedInitialEvent(forwardedInitialEvent); } // start fires first execution which should execute initial transition @@ -433,6 +437,11 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo stateMachineExecutor.execute(); } + @Override + public void setForwardedInitialEvent(Message message) { + forwardedInitialEvent = message; + } + private StateMachine getRelayStateMachine() { return relay != null ? relay : this; } 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 064a036c..747fdfdd 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 @@ -91,6 +91,8 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im private final StateMachineInterceptorList interceptors = new StateMachineInterceptorList(); + private volatile Message forwardedInitialEvent; + /** * Instantiates a new default state machine executor. * @@ -161,6 +163,11 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im initialHandled.set(!enabled); } + @Override + public void setForwardedInitialEvent(Message message) { + forwardedInitialEvent = message; + } + @Override public void addStateMachineInterceptor(StateMachineInterceptor interceptor) { interceptors.add(interceptor); @@ -265,7 +272,12 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im if (!initialHandled.getAndSet(true)) { ArrayList> trans = new ArrayList>(); trans.add(initialTransition); - handleInitialTrans(initialTransition, initialEvent); + // TODO: should we merge if initial event is actually used? + if (initialEvent != null) { + handleInitialTrans(initialTransition, initialEvent); + } else { + handleInitialTrans(initialTransition, forwardedInitialEvent); + } return; } log.debug("Process trigger queue"); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java index 0e514b63..7f3951e4 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java @@ -18,6 +18,7 @@ package org.springframework.statemachine.support; import org.springframework.messaging.Message; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.access.StateMachineAccess; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.trigger.Trigger; @@ -79,6 +80,14 @@ public interface StateMachineExecutor { */ void stop(); + /** + * Set initial forwarded event. + * + * @param message the forwarded message + * @see StateMachineAccess#setForwardedInitialEvent(Message) + */ + void setForwardedInitialEvent(Message message); + /** * Sets the state machine executor transit. * diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/EventHeaderTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EventHeaderTests.java new file mode 100644 index 00000000..67fa1b83 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EventHeaderTests.java @@ -0,0 +1,286 @@ +/* + * 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; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +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; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +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; + +public class EventHeaderTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @SuppressWarnings("unchecked") + @Test + public void testHeaderPassedToInitialInSubs1() throws InterruptedException { + context.register(Config1.class); + context.refresh(); + StateMachine machine = context.getBean(StateMachine.class); + HeaderTestAction headerTestAction1I = context.getBean("headerTestAction1I", HeaderTestAction.class); + HeaderTestAction headerTestAction1 = context.getBean("headerTestAction1", HeaderTestAction.class); + HeaderTestAction headerTestAction11 = context.getBean("headerTestAction11", HeaderTestAction.class); + HeaderTestAction headerTestAction111 = context.getBean("headerTestAction111", HeaderTestAction.class); + HeaderTestAction headerTestAction112 = context.getBean("headerTestAction112", HeaderTestAction.class); + TestListener listener = new TestListener(); + listener.reset(1); + machine.addStateListener(listener); + machine.start(); + + assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + + listener.reset(3); + machine.sendEvent(MessageBuilder.withPayload("E1").setHeader("testHeader", "testValue").build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(3)); + + assertThat(headerTestAction1I.testHeader, is("testValue")); + assertThat(headerTestAction1.testHeader, is("testValue")); + assertThat(headerTestAction11.testHeader, is("testValue")); + assertThat(headerTestAction111.testHeader, is("testValue")); + assertThat(headerTestAction112.testHeader, nullValue()); + + headerTestAction1.testHeader = null; + headerTestAction11.testHeader = null; + headerTestAction111.testHeader = null; + + listener.reset(1); + machine.sendEvent(MessageBuilder.withPayload("E2").setHeader("testHeader", "testValue").build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + + assertThat(headerTestAction1.testHeader, nullValue()); + assertThat(headerTestAction11.testHeader, nullValue()); + assertThat(headerTestAction111.testHeader, nullValue()); + assertThat(headerTestAction112.testHeader, is("testValue")); + } + + @SuppressWarnings("unchecked") + @Test + public void testHeaderPassedToInitialInSubs2() throws InterruptedException { + context.register(Config1.class); + context.refresh(); + StateMachine machine = context.getBean(StateMachine.class); + HeaderTestAction headerTestAction1 = context.getBean("headerTestAction1", HeaderTestAction.class); + HeaderTestAction headerTestAction11 = context.getBean("headerTestAction11", HeaderTestAction.class); + HeaderTestAction headerTestAction111 = context.getBean("headerTestAction111", HeaderTestAction.class); + HeaderTestAction headerTestAction112 = context.getBean("headerTestAction112", HeaderTestAction.class); + TestListener listener = new TestListener(); + listener.reset(1); + machine.addStateListener(listener); + machine.start(); + + assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + + listener.reset(3); + machine.sendEvent(MessageBuilder.withPayload("E1").setHeader("testHeader", "testValue").build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(3)); + + assertThat(headerTestAction1.testHeader, is("testValue")); + assertThat(headerTestAction11.testHeader, is("testValue")); + assertThat(headerTestAction111.testHeader, is("testValue")); + assertThat(headerTestAction112.testHeader, nullValue()); + + headerTestAction1.testHeader = null; + headerTestAction11.testHeader = null; + headerTestAction111.testHeader = null; + + listener.reset(1); + machine.sendEvent(MessageBuilder.withPayload("E2").build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + + assertThat(headerTestAction1.testHeader, nullValue()); + assertThat(headerTestAction11.testHeader, nullValue()); + assertThat(headerTestAction111.testHeader, nullValue()); + assertThat(headerTestAction112.testHeader, nullValue()); + } + + @SuppressWarnings("unchecked") + @Test + public void testHeaderPassedToInitialInSubs3() throws InterruptedException { + context.register(Config1.class); + context.refresh(); + StateMachine machine = context.getBean(StateMachine.class); + HeaderTestAction headerTestAction1I = context.getBean("headerTestAction1I", HeaderTestAction.class); + HeaderTestAction headerTestAction1 = context.getBean("headerTestAction1", HeaderTestAction.class); + HeaderTestAction headerTestAction11 = context.getBean("headerTestAction11", HeaderTestAction.class); + HeaderTestAction headerTestAction111 = context.getBean("headerTestAction111", HeaderTestAction.class); + HeaderTestAction headerTestAction112 = context.getBean("headerTestAction112", HeaderTestAction.class); + TestListener listener = new TestListener(); + listener.reset(1); + machine.addStateListener(listener); + machine.start(); + + assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + + listener.reset(3); + machine.sendEvent(MessageBuilder.withPayload("E1").setHeader("testHeader", "testValue").build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(3)); + + listener.reset(1); + machine.sendEvent(MessageBuilder.withPayload("E2").setHeader("testHeader", "testValue").build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + + headerTestAction1I.testHeader = null; + headerTestAction1.testHeader = null; + headerTestAction11.testHeader = null; + headerTestAction111.testHeader = null; + headerTestAction112.testHeader = null; + listener.reset(1); + machine.sendEvent(MessageBuilder.withPayload("E3").setHeader("testHeader", "testValue").build()); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + + assertThat(headerTestAction1I.testHeader, nullValue()); + assertThat(headerTestAction1.testHeader, nullValue()); + assertThat(headerTestAction11.testHeader, nullValue()); + assertThat(headerTestAction111.testHeader, is("testValue")); + assertThat(headerTestAction112.testHeader, nullValue()); + } + + @Configuration + @EnableStateMachine + static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("SI") + .state("S1", headerTestAction1(), null) + .and() + .withStates() + .parent("S1") + .initial("S11", headerTestAction1I()) + .state("S11", headerTestAction11(), null) + .state("S12") + .and() + .withStates() + .parent("S11") + .initial("S111") + .state("S111", headerTestAction111(), null) + .state("S122", headerTestAction112(), null); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("SI") + .target("S1") + .event("E1") + .and() + .withExternal() + .source("S111") + .target("S122") + .event("E2") + .and() + .withExternal() + .source("S122") + .target("S111") + .event("E3"); + } + + @Bean + public HeaderTestAction headerTestAction1I() { + return new HeaderTestAction(); + } + + @Bean + public HeaderTestAction headerTestAction1() { + return new HeaderTestAction(); + } + + @Bean + public HeaderTestAction headerTestAction11() { + return new HeaderTestAction(); + } + + @Bean + public HeaderTestAction headerTestAction111() { + return new HeaderTestAction(); + } + + @Bean + public HeaderTestAction headerTestAction112() { + return new HeaderTestAction(); + } + } + + private static class HeaderTestAction implements Action { + + String testHeader = null; + + @Override + public void execute(StateContext context) { + testHeader = context.getMessageHeaders().get("testHeader", String.class); + } + + } + + private static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile int stateChangedCount = 0; + volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1); + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + stateMachineStartedLatch.countDown(); + } + + @Override + public void stateChanged(State from, State to) { + stateChangedCount++; + stateChangedLatch.countDown(); + } + + public void reset(int c1) { + stateChangedLatch = new CountDownLatch(c1); + stateChangedCount = 0; + } + + } + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java index 570cd15a..89aa914b 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java @@ -178,6 +178,10 @@ public class StateMachineAccessTests { return null; } + @Override + public void setForwardedInitialEvent(Message message) { + } + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java index 83173dc1..eb470d14 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java @@ -206,10 +206,10 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { } // end::snippetD[] -// tag::snippetE[] +// tag::snippetEA[] @Configuration @EnableStateMachine - public static class Config5 + public static class Config51 extends EnumStateMachineConfigurerAdapter { @Override @@ -235,7 +235,38 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { } } -// end::snippetE[] +// end::snippetEA[] + +// tag::snippetEB[] + @Configuration + @EnableStateMachine + public static class Config52 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1, action()) + .state(States.S1, action(), null) + .state(States.S2, null, action()) + .state(States.S3, action(), action()); + } + + @Bean + public Action action() { + return new Action() { + + @Override + public void execute(StateContext context) { + // do something + } + }; + } + + } +// end::snippetEB[] // tag::snippetFA[] @Configuration