Forward event headers to initial transition

- Enhancing internal model so that event and its headers can
  be passed into action if it's used in an initial sub-state
  when transition target its super state.
- Added tests and better docs.
- Fixes #99.
This commit is contained in:
Janne Valkealahti
2015-08-28 08:11:49 +01:00
parent 166c586458
commit 1371ceac7b
9 changed files with 422 additions and 10 deletions

View File

@@ -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

View File

@@ -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<S, E> {
*/
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<E> message);
}

View File

@@ -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<S, E> extends AbstractState<S, E> {
}
@Override
public void entry(StateContext<S, E> context) {
public void entry(final StateContext<S, E> context) {
Collection<? extends Action<S, E>> actions = getEntryActions();
if (actions != null && !isLocal(context)) {
for (Action<S, E> action : actions) {
@@ -166,8 +167,20 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
State<S, E> target = context.getTransition().getTarget();
State<S, E> immediateDeepParent = findDeepParent(getSubmachine().getStates(), target);
if (context.getEvent() != null) {
getSubmachine().getStateMachineAccessor().doWithRegion(
new StateMachineFunction<StateMachineAccess<S, E>>() {
@Override
public void apply(StateMachineAccess<S, E> 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<S, E>) immediateDeepParent).getSubmachine().getStateMachineAccessor()
.doWithRegion(new StateMachineFunction<StateMachineAccess<S, E>>() {
@@ -189,8 +202,8 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
}
});
} else if (immediateDeepParent != null && isInitial(immediateDeepParent) && isInitial(target)) {
((StateMachineState<S, E>) immediateDeepParent).getSubmachine().getStateMachineAccessor().doWithRegion(
new StateMachineFunction<StateMachineAccess<S, E>>() {
((StateMachineState<S, E>) immediateDeepParent).getSubmachine().getStateMachineAccessor()
.doWithRegion(new StateMachineFunction<StateMachineAccess<S, E>>() {
@Override
public void apply(StateMachineAccess<S, E> function) {

View File

@@ -113,6 +113,8 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
private String id = UUID.randomUUID().toString();
private volatile Message<E> forwardedInitialEvent;
/**
* Instantiates a new abstract state machine.
*
@@ -294,6 +296,8 @@ public abstract class AbstractStateMachine<S, E> 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<S, E> extends StateMachineObjectSuppo
stateMachineExecutor.execute();
}
@Override
public void setForwardedInitialEvent(Message<E> message) {
forwardedInitialEvent = message;
}
private StateMachine<S, E> getRelayStateMachine() {
return relay != null ? relay : this;
}

View File

@@ -91,6 +91,8 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
private final StateMachineInterceptorList<S, E> interceptors =
new StateMachineInterceptorList<S, E>();
private volatile Message<E> forwardedInitialEvent;
/**
* Instantiates a new default state machine executor.
*
@@ -161,6 +163,11 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
initialHandled.set(!enabled);
}
@Override
public void setForwardedInitialEvent(Message<E> message) {
forwardedInitialEvent = message;
}
@Override
public void addStateMachineInterceptor(StateMachineInterceptor<S, E> interceptor) {
interceptors.add(interceptor);
@@ -265,7 +272,12 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
if (!initialHandled.getAndSet(true)) {
ArrayList<Transition<S, E>> trans = new ArrayList<Transition<S, E>>();
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");

View File

@@ -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<S, E> {
*/
void stop();
/**
* Set initial forwarded event.
*
* @param message the forwarded message
* @see StateMachineAccess#setForwardedInitialEvent(Message)
*/
void setForwardedInitialEvent(Message<E> message);
/**
* Sets the state machine executor transit.
*

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> 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<String, String> 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, String> {
String testHeader = null;
@Override
public void execute(StateContext<String, String> context) {
testHeader = context.getMessageHeaders().get("testHeader", String.class);
}
}
private static class TestListener extends StateMachineListenerAdapter<String, String> {
volatile CountDownLatch stateChangedLatch = new CountDownLatch(1);
volatile int stateChangedCount = 0;
volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1);
@Override
public void stateMachineStarted(StateMachine<String, String> stateMachine) {
stateMachineStartedLatch.countDown();
}
@Override
public void stateChanged(State<String, String> from, State<String, String> to) {
stateChangedCount++;
stateChangedLatch.countDown();
}
public void reset(int c1) {
stateChangedLatch = new CountDownLatch(c1);
stateChangedCount = 0;
}
}
}

View File

@@ -178,6 +178,10 @@ public class StateMachineAccessTests {
return null;
}
@Override
public void setForwardedInitialEvent(Message<String> message) {
}
}
}

View File

@@ -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<States, Events> {
@Override
@@ -235,7 +235,38 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests {
}
}
// end::snippetE[]
// end::snippetEA[]
// tag::snippetEB[]
@Configuration
@EnableStateMachine
public static class Config52
extends EnumStateMachineConfigurerAdapter<States, Events> {
@Override
public void configure(StateMachineStateConfigurer<States, Events> 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<States, Events> action() {
return new Action<States, Events>() {
@Override
public void execute(StateContext<States, Events> context) {
// do something
}
};
}
}
// end::snippetEB[]
// tag::snippetFA[]
@Configuration