From 4aa18a4ca56a8e53b753e3de640b0703e9204afb Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Thu, 31 Dec 2015 17:39:57 +0000 Subject: [PATCH] Fix some StateContext use cases - This is part 1 of changes for #150. - Modify AbstractStateMachine to pass context in part of a stages. - Add some tests - Add preliminary docs --- docs/src/reference/asciidoc/index.adoc | 1 + docs/src/reference/asciidoc/sm.adoc | 49 ++++++++- .../support/AbstractStateMachine.java | 16 +-- .../support/DefaultStateContext.java | 4 +- .../statemachine/StateContextTests.java | 104 +++++++++++++++++- .../docs/DocsConfigurationSampleTests.java | 4 + 6 files changed, 159 insertions(+), 19 deletions(-) diff --git a/docs/src/reference/asciidoc/index.adoc b/docs/src/reference/asciidoc/index.adoc index b987d7b1..73e7ee9d 100644 --- a/docs/src/reference/asciidoc/index.adoc +++ b/docs/src/reference/asciidoc/index.adoc @@ -10,6 +10,7 @@ :core-jdbc: http://docs.spring.io/spring/docs/{spring-version}/spring-framework-reference/html/jdbc.html :core-jdbc-JdbcTemplate: http://docs.spring.io/spring/docs/{spring-version}/spring-framework-reference/html/jdbc.html#jdbc-JdbcTemplate :sm-statecontext: http://docs.spring.io/spring-statemachine/docs/{spring-statemachine-version}/api/org/springframework/statemachine/StateContext.html +:sm-statecontext-stage: http://docs.spring.io/spring-statemachine/docs/{spring-statemachine-version}/api/org/springframework/statemachine/StateContext.Stage.html = Spring Statemachine - Reference Documentation diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index 8dfd2a6d..d590292e 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -633,11 +633,46 @@ include::samples/DocsConfigurationSampleTests.java[tags=snippet6] [[sm-statecontext]] == Using StateContext -{sm-statecontext}[_StateContext_] is a domain object representing a current status of a -state machine within a transition or an action. Context gives an -access to a various information like event, message headers, extended -state variables, current transition and a top-level state machine in -case there is a need to send events to a further processing. +{sm-statecontext}[_StateContext_] is a one of a most important objects +when working with a state machine as it is passed into various methods +and callbacks to give status of a current state of a state machine and +where it is possibly going. If simplifying things a little it can be +considered to be a snapshot of a current state machine stage where it +is at a time _StateContext_ is passed on. + +[NOTE] +==== +In `Spring Statemachine 1.0.x` _StateContext_ usage were relatively naive +in terms of how it was used to just pass stuff around as a simple `POJO`. +Starting from `Spring Statemachine 1.1.x` its role has been greatly +improved by making it a first class citizen in a state machine. +==== + +In overall _StateContext_ can be used as. + +* Access to current `Message`, `Event` or their + `MessageHeaders` if known. +* Access to state machine `Extended State`. +* Access to `StateMachine` itself. +* Access to possible state machine error. +* Access to current `Transition` if applicable. +* Access to _source_ and _target_ states where state machine is + possibly getting from and going to. +* Access to current `Stage` as described in <>. + +_StateContext_ is passed into various components interacting with user +like `Action` and `Guard`. + +[[sm-statecontext-stage]] +=== Stages +{sm-statecontext-stage}[_Stage_] is representation of a `stage` on +which a state machine is currently interacting with a user. Current +stages are `EVENT_NOT_ACCEPTED`, `EXTENDED_STATE_CHANGED`, +`STATE_CHANGED`, `STATE_ENTRY`, `STATE_EXIT`, `STATEMACHINE_ERROR`, +`STATEMACHINE_START`, `STATEMACHINE_STOP`, `TRANSITION`, +`TRANSITION_START` and `TRANSITION_END` which look very familiar as +those match how user can interact with listeners as described in +<>. [[sm-triggers]] == Triggering Transitions @@ -737,6 +772,10 @@ In above example we simply created our own listener class _StateMachineEventListener_ which extends _StateMachineListenerAdapter_. +Listener method `stateContext` gives an access to various +_StateContext_ changes on a different stages. More about about it in +section <>. + Once you have your own listener defined, it can be registered into a state machine via its interface as shown below. It's just a matter of flavour if it's hooked up within a spring configuration or done 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 d3036e26..e0c91054 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 @@ -261,18 +261,18 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo executor.setStateMachineExecutorTransit(new StateMachineExecutorTransit() { @Override - public void transit(Transition t, StateContext stateContext, Message queuedMessage) { + public void transit(Transition t, StateContext ctx, Message message) { // TODO: fix above stateContext as it's not used - notifyTransitionStart(t, queuedMessage, buildStateContext(Stage.TRANSITION_START, queuedMessage, null, getRelayStateMachine())); - notifyTransition(t, queuedMessage, buildStateContext(Stage.TRANSITION, queuedMessage, null, getRelayStateMachine())); + notifyTransitionStart(t, message, buildStateContext(Stage.TRANSITION_START, message, t, getRelayStateMachine())); + notifyTransition(t, message, buildStateContext(Stage.TRANSITION, message, t, getRelayStateMachine())); if (t.getKind() == TransitionKind.INITIAL) { - switchToState(t.getTarget(), queuedMessage, t, getRelayStateMachine()); - notifyStateMachineStarted(getRelayStateMachine(), buildStateContext(Stage.STATEMACHINE_START, queuedMessage, null, getRelayStateMachine())); + switchToState(t.getTarget(), message, t, getRelayStateMachine()); + notifyStateMachineStarted(getRelayStateMachine(), buildStateContext(Stage.STATEMACHINE_START, message, t, getRelayStateMachine())); } else if (t.getKind() != TransitionKind.INTERNAL) { - switchToState(t.getTarget(), queuedMessage, t, getRelayStateMachine()); + switchToState(t.getTarget(), message, t, getRelayStateMachine()); } // TODO: looks like events should be called here and anno processing earlier - notifyTransitionEnd(t, queuedMessage, buildStateContext(Stage.TRANSITION_END, queuedMessage, null, getRelayStateMachine())); + notifyTransitionEnd(t, message, buildStateContext(Stage.TRANSITION_END, message, t, getRelayStateMachine())); } }); stateMachineExecutor = executor; @@ -889,7 +889,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } } - notifyStateEntered(state, message, buildStateContext(Stage.STATE_ENTRY, message, null, getRelayStateMachine(), null, state)); + notifyStateEntered(state, message, buildStateContext(Stage.STATE_ENTRY, message, transition, getRelayStateMachine(), null, state)); log.debug("Enter state=[" + state + "]"); state.entry(stateContext); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java index 21af2cea..f68f635c 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateContext.java @@ -116,12 +116,12 @@ public class DefaultStateContext implements StateContext { @Override public State getSource() { - return source; + return source != null ? source : (transition != null ? transition.getSource() : null); } @Override public State getTarget() { - return target; + return target != null ? target : (transition != null ? transition.getTarget() : null); } @Override diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateContextTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateContextTests.java index 57fffacd..31aedbd9 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateContextTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateContextTests.java @@ -15,17 +15,25 @@ */ package org.springframework.statemachine; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import java.util.ArrayList; import java.util.Map; +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; 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.statemachine.StateContext.Stage; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; @@ -41,11 +49,11 @@ public class StateContextTests extends AbstractStateMachineTests { return new AnnotationConfigApplicationContext(); } + @SuppressWarnings("unchecked") @Test public void testStartCycles() throws Exception { context.register(Config1.class); context.refresh(); - @SuppressWarnings("unchecked") StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); TestStateMachineListener listener = new TestStateMachineListener(); @@ -53,8 +61,87 @@ public class StateContextTests extends AbstractStateMachineTests { machine.start(); assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0, States.S1, States.S11)); - assertThat(listener.contexts.size(), is(19)); - // TODO: continue with other tests to verify context fields + assertThat(listener.contexts, hasSize(19)); + + assertThat(listener.contexts, contains( + hasStage(Stage.EXTENDED_STATE_CHANGED), + hasStage(Stage.TRANSITION_START), + hasStage(Stage.TRANSITION), + hasStage(Stage.STATE_ENTRY), + hasStage(Stage.TRANSITION_START), + hasStage(Stage.TRANSITION), + hasStage(Stage.STATE_ENTRY), + hasStage(Stage.TRANSITION_START), + hasStage(Stage.TRANSITION), + hasStage(Stage.STATE_ENTRY), + hasStage(Stage.STATE_CHANGED), + hasStage(Stage.STATEMACHINE_START), + hasStage(Stage.TRANSITION_END), + hasStage(Stage.STATE_CHANGED), + hasStage(Stage.STATEMACHINE_START), + hasStage(Stage.TRANSITION_END), + hasStage(Stage.STATE_CHANGED), + hasStage(Stage.STATEMACHINE_START), + hasStage(Stage.TRANSITION_END) + )); + + assertThat(listener.contexts.get(0).getStage(), is(Stage.EXTENDED_STATE_CHANGED)); + + assertThat(listener.contexts.get(1).getStage(), is(Stage.TRANSITION_START)); + + assertThat(listener.contexts.get(2).getStage(), is(Stage.TRANSITION)); + assertThat(listener.contexts.get(2).getTransition(), notNullValue()); + assertThat(listener.contexts.get(2).getTransition().getSource(), nullValue()); + assertThat(listener.contexts.get(2).getTransition().getTarget(), notNullValue()); + assertThat(listener.contexts.get(2).getTransition().getTarget().getId(), is(States.S0)); + assertThat(listener.contexts.get(2).getSource(), nullValue()); + assertThat(listener.contexts.get(2).getTarget(), notNullValue()); + + + assertThat(listener.contexts.get(3).getStage(), is(Stage.STATE_ENTRY)); + assertThat(listener.contexts.get(3).getTarget(), notNullValue()); + assertThat(listener.contexts.get(3).getTarget().getId(), is(States.S0)); + assertThat(listener.contexts.get(3).getTransition(), notNullValue()); + + assertThat(listener.contexts.get(4).getStage(), is(Stage.TRANSITION_START)); + + assertThat(listener.contexts.get(5).getStage(), is(Stage.TRANSITION)); + + assertThat(listener.contexts.get(6).getStage(), is(Stage.STATE_ENTRY)); + assertThat(listener.contexts.get(6).getTarget(), notNullValue()); + assertThat(listener.contexts.get(6).getTarget().getId(), is(States.S1)); +// assertThat(listener.contexts.get(6).getTransition(), notNullValue()); + + assertThat(listener.contexts.get(7).getStage(), is(Stage.TRANSITION_START)); + + assertThat(listener.contexts.get(8).getStage(), is(Stage.TRANSITION)); + + assertThat(listener.contexts.get(9).getStage(), is(Stage.STATE_ENTRY)); + assertThat(listener.contexts.get(9).getTarget(), notNullValue()); + assertThat(listener.contexts.get(9).getTarget().getId(), is(States.S11)); +// assertThat(listener.contexts.get(9).getTransition(), notNullValue()); + + assertThat(listener.contexts.get(10).getStage(), is(Stage.STATE_CHANGED)); + + assertThat(listener.contexts.get(11).getStage(), is(Stage.STATEMACHINE_START)); + assertThat(listener.contexts.get(11).getTransition(), notNullValue()); + + assertThat(listener.contexts.get(12).getStage(), is(Stage.TRANSITION_END)); + + assertThat(listener.contexts.get(13).getStage(), is(Stage.STATE_CHANGED)); + + assertThat(listener.contexts.get(14).getStage(), is(Stage.STATEMACHINE_START)); + assertThat(listener.contexts.get(14).getTransition(), notNullValue()); + + assertThat(listener.contexts.get(15).getStage(), is(Stage.TRANSITION_END)); + + assertThat(listener.contexts.get(16).getStage(), is(Stage.STATE_CHANGED)); + + assertThat(listener.contexts.get(17).getStage(), is(Stage.STATEMACHINE_START)); + assertThat(listener.contexts.get(17).getTransition(), notNullValue()); + + assertThat(listener.contexts.get(18).getStage(), is(Stage.TRANSITION_END)); +// assertThat(listener.contexts.get(18).getTransition(), notNullValue()); } static class TestStateMachineListener extends StateMachineListenerAdapter { @@ -67,6 +154,15 @@ public class StateContextTests extends AbstractStateMachineTests { } } + private static Matcher> hasStage(final Stage stage) { + return new FeatureMatcher, Stage>(equalTo(stage), "stage", "stage") { + @Override + protected Stage featureValueOf(final StateContext actual) { + return actual.getStage(); + } + }; + } + @Configuration @EnableStateMachine static class Config1 extends EnumStateMachineConfigurerAdapter { 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 3a313076..122c4e99 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 @@ -396,6 +396,10 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { @Override public void stateMachineError(StateMachine stateMachine, Exception exception) { } + + @Override + public void stateContext(StateContext stateContext) { + } } // end::snippetH[]