diff --git a/build.gradle b/build.gradle index 6af1e33e..5a01c1a4 100644 --- a/build.gradle +++ b/build.gradle @@ -181,6 +181,7 @@ configure(rootProject) { task copyDocsSamples(type: Copy) { from 'spring-statemachine-core/src/test/java/org/springframework/statemachine/docs' from 'spring-statemachine-samples/src/main/java/' + from 'spring-statemachine-samples/washer/src/main/java/' from 'spring-statemachine-samples/tasks/src/main/java/' from 'spring-statemachine-samples/turnstile/src/main/java/' from 'spring-statemachine-samples/showcase/src/main/java/' diff --git a/docs/src/reference/asciidoc/appendix.adoc b/docs/src/reference/asciidoc/appendix.adoc index 96e07c4f..9897e098 100644 --- a/docs/src/reference/asciidoc/appendix.adoc +++ b/docs/src/reference/asciidoc/appendix.adoc @@ -15,6 +15,16 @@ material in this reference documentation. include::samples/States.java[tags=snippetA] ---- +[source,java,indent=0] +---- +include::samples/States2.java[tags=snippetA] +---- + +[source,java,indent=0] +---- +include::samples/States3.java[tags=snippetA] +---- + [source,java,indent=0] ---- include::samples/Events.java[tags=snippetA] @@ -86,6 +96,22 @@ directly contained in a state machine and all other regions in the state machine also are completed, then it means that the entire state machine is completed. +*History State*:: +A pseudo state which allows a state machine to remember its last +active state. Two types of history state exists, _shallow_ which only +remember top level state and _deep_ which remembers active states in a +sub-machines. + +*Choice State*:: +A pseudo state which allows to make a transition choice based of i.e. +event headers or extended state variables. + +*Fork State*:: +A pseudo state which gives a controlled entry into a regions. + +*Join State*:: +A pseudo state which gives a controlled exit from a regions. + *Region*:: A region is an orthogonal part of either a composite state or a state machine. It contains states and transitions. @@ -124,6 +150,45 @@ flags, nested if/else/break clauses or other impractical logic you simply rely on state, state variables or other interaction with a state machine. +==== Pseudo States +PseudoState is a special type of state which usually introduces more +higher level logic into a state machine by either giving a state a +special meaning like initial state. State machine can then internally +react to these states by doing various actions available in UML state +machine concepts. + +===== Initial +Initial state is always needed for every single state machine whether +you have a simple one level state machine or more complex state +machine composed with submachines or regions. Initial state simple +defines where state machine should go when it starts and without it +state machine is ill-formed. + +===== End +Terminate state which is also called as end state will indicate that a +particular state machine has reached its final state. Effectively this +mean that a state machine will no longer process any events and will +not transit to any other state. However in a case of submachines are +regions, state machine is able to restart from its terminal state. + +===== Choice +Choice can be used to choose a transition conditionally. + +===== History +History state can be used to remember a last active state +configuration. After state machine has been exited, history state can +be used to restore previous knows configuration. There are two types +of history states available, _SHALLOW_ only remember active state of a +state machine itself while _DEEP_ also remembers nested states. + +===== Fork +Fork can be used to do an explicit entry into one or more regions. + +===== Join +Join is used to merge several transitions together originating from +different regions. It it generally used to wait and block for +participating regions to get into its join target states. + ==== Guard Conditions Guard conditions are expressions which evaluates either to *TRUE* or *FALSE* based on extended state variables and event parameters. Guards diff --git a/docs/src/reference/asciidoc/images/statechart5.png b/docs/src/reference/asciidoc/images/statechart5.png new file mode 100644 index 00000000..4a02b9c0 Binary files /dev/null and b/docs/src/reference/asciidoc/images/statechart5.png differ diff --git a/docs/src/reference/asciidoc/images/statechart6.png b/docs/src/reference/asciidoc/images/statechart6.png new file mode 100644 index 00000000..cea601c8 Binary files /dev/null and b/docs/src/reference/asciidoc/images/statechart6.png differ diff --git a/docs/src/reference/asciidoc/sm-examples.adoc b/docs/src/reference/asciidoc/sm-examples.adoc index 403ca2a7..cc67587f 100644 --- a/docs/src/reference/asciidoc/sm-examples.adoc +++ b/docs/src/reference/asciidoc/sm-examples.adoc @@ -442,3 +442,183 @@ What happened in above run: * We print lcd status and request next track. * We stop playing. +== Tasks + +Tasks is a sample demonstrating a parallel task handling within a +regions and additionally adds an error handling to either +automatically or manually fixing task problems before continuing back +to a state where tasks can be run again. + +image::images/statechart5.png[width=500] + +On a high level what happens in this state machine is: + +* We're always trying to get into READY state so that we can use event + RUN to execute tasks. +* TASKS state which is composed with 3 independent regions has been + put in a middle of FORK and JOIN states which will cause regions to + go into its initial states and to be joined by end states. +* From JOIN state we go automatically into a CHOICE state which checks + existence of error flags in extended state variables. Tasks can set + these flags and it gives CHOICE state a possibility to go into ERROR + state where errors can be handled either automatically or manually. +* AUTOMATIC state in ERROR can try to automatically fix error and goes + back to READY if it succeed to do so. If error is something what + can't be handled automatically, user intervention is needed and + machine is put into MANUAL state via FALLBACK event. + +.States +[source,java,indent=0] +---- +include::samples/demo/tasks/Application.java[tags=snippetB] +---- + +.Events +[source,java,indent=0] +---- +include::samples/demo/tasks/Application.java[tags=snippetC] +---- + +.Configuration - states +[source,java,indent=0] +---- +include::samples/demo/tasks/Application.java[tags=snippetAA] +---- + +.Configuration - transitions +[source,java,indent=0] +---- +include::samples/demo/tasks/Application.java[tags=snippetAB] +---- + +Guard below is guarding choice entry into a ERROR state and needs to +return TRUE if error has happened. For this guard simply checks that +all extended state variables(T1, T2 and T3) are TRUE. + +[source,java,indent=0] +---- +include::samples/demo/tasks/Application.java[tags=snippetAC] +---- + +[source,java,indent=0] +---- +include::samples/demo/tasks/Application.java[tags=snippetAD] +---- + +[source,java,indent=0] +---- +include::samples/demo/tasks/Application.java[tags=snippetAE] +---- + +[source,java,indent=0] +---- +include::samples/demo/tasks/Application.java[tags=snippetAF] +---- + +Lets see an example how this state machine actually works. + +[source,text] +---- +sm>sm start +State machine started +Entry state READY + +sm>tasks run +Entry state TASKS +run task on T1 +run task on T2 +run task on T3 +run task on T1 done +run task on T2 done +run task on T3 done +Entry state T1 +Entry state T3 +Entry state T2 +Entry state T1E +Entry state T2E +Entry state T3E +Exit state TASKS +Entry state JOIN +Exit state JOIN +Entry state READY + +sm> +---- + +== Washer + +Washer is a sample demonstrating a use of a history state to recover a +running state configuration with a simulated power off situation. + +Anyone ever used a washing machine knows that if you can somehow pause +the program it will continue from a same state when lid is closed. +This kind of behaviour can be implemented in a state machine by using +a history pseudo state. + +image::images/statechart6.png[width=500] + +.States +[source,java,indent=0] +---- +include::samples/demo/washer/Application.java[tags=snippetB] +---- + +.Events +[source,java,indent=0] +---- +include::samples/demo/washer/Application.java[tags=snippetC] +---- + +.Configuration - states +[source,java,indent=0] +---- +include::samples/demo/washer/Application.java[tags=snippetAA] +---- + +.Configuration - transitions +[source,java,indent=0] +---- +include::samples/demo/washer/Application.java[tags=snippetAB] +---- + +Lets see an example how this state machine actually works. +[source,text] +---- +sm>sm start +Entry state RUNNING +Entry state WASHING +State machine started + +sm>sm event RINSE +Exit state WASHING +Entry state RINSING +Event RINSE send + +sm>sm event DRY +Exit state RINSING +Entry state DRYING +Event DRY send + +sm>sm event CUTPOWER +Exit state DRYING +Exit state RUNNING +Entry state POWEROFF +Event CUTPOWER send + +sm>sm event RESTOREPOWER +Exit state POWEROFF +Entry state RUNNING +Entry state WASHING +Entry state DRYING +Event RESTOREPOWER send +---- + +What happened in above run: + +* State machine is started which causes machine to get initialized. +* We go to RINSING state. +* We go to DRYING state. +* We cut power and go to POWEROFF state. +* State is restored via HISTORY state which takes state machine back + to its previous known state. + diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index 83ec552c..976e0115 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -22,6 +22,15 @@ Statemachine is configured and how it leverages Spring's lightweight IoC containers to simplify the application internals to make it more manageable. +[NOTE] +==== +Configuration examples in this section are not feature complete, i.e. +you always need to have definitions of both states and transitions, +otherwise state machine configuration would be ill-formed. We have +simply made code snippets less verbose by leaving other needed parts +away. +==== + === Configuring States We'll get into more complex configuration examples a bit later but lets first start with a something simple. For most simple state @@ -43,6 +52,20 @@ particular states are sub-states of some other state. include::samples/DocsConfigurationSampleTests.java[tags=snippetB] ---- +=== Configuring Regions + +There are no special configuration methods to mark a collection of +states to be part of a orthogonal state. To put it simple, orthogonal +state is created when same hierarchical state machine has multiple set +of states each having a initial state. Because a individual state +machine can only have one initial state, multiple initial states mush +mean that a specific state mush have multiple independent regions. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetP] +---- + === Configuring Transitions We support three different types of transitions, `external`, `internal` and `local`. Transitions are either triggered by a signal @@ -80,6 +103,93 @@ Actions can be defined with various steps within a state transitions. include::samples/DocsConfigurationSampleTests.java[tags=snippetE] ---- +=== Configuring Pseudo States + +_Pseudo state_ configuration is usually done by configuring states and +transitions. Pseudo states are automatically added to state machine as +states. + +==== Initial State +Simply mark a particular state as initial state by using `initial()` +method. There are two methods where one takes extra argument to define +an initial action. This initial action is good for example initialize +extended state variables. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetQ] +---- + +==== Terminate State +Simply mark a particular state as end state by using `end()` method. +This can be done max one time per individual sub-machine or region. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetA] +---- + +==== History State +History state can be defined once for each individual state machine. +You need to choose its state identifier and `History.SHALLOW` or +`History.DEEP` respectively. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetR] +---- + +==== Choice State +Choice needs to be defined in both states and transitions to work +properly. Mark particular state as choice state by using `choice()` +method. This state needs to match source state when transition is +configured for this choice. + +Transition is configured using `withChoice()` where you define source +state and `first/then/last` structure which is equivalent to normal +`if/elseif/else`. With `first` and `then` you can specify a guard just +like you'd use a condition with `if/elseif` clauses. + +Transition needs to be able to exist so make sure `last` is used. +Otherwise configuration is ill-formed. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetS] +---- + +==== Fork State +Fork needs to be defined in both states and transitions to work +properly. Mark particular state as choice state by using `fork()` +method. This state needs to match source state when transition is +configured for this fork. + +Target state needs to be a super state or immediate states in +regions. Using a super state as target will take all regions into +initial states. Targeting individual state give more controlled entry +into regions. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetT] +---- + +==== Join State +Join needs to be defined in both states and transitions to work +properly. Mark particular state as choice state by using `join()` +method. This state doesn't need to match either source states or +target state in a transition configuretion. + +Select one target state where transition goes when all source states +has been joined. If you use state hosting regions as source, end +states of a regions are used as joins. Otherwise you can pick any +states from a regions. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetU] +---- + [[sm-factories]] == State Machine Factories There are use cases when state machine needs to be created dynamically 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 b91beed0..0b67911f 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 @@ -39,6 +39,7 @@ import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter import org.springframework.statemachine.config.StateMachineFactory; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.config.configurers.StateConfigurer.History; import org.springframework.statemachine.event.StateMachineEvent; import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.listener.StateMachineListenerAdapter; @@ -354,4 +355,223 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { } +// tag::snippetP[] + @Configuration + @EnableStateMachine + public static class Config10 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States2.S1) + .state(States2.S2) + .and() + .withStates() + .parent(States2.S2) + .initial(States2.S2I) + .state(States2.S21) + .end(States2.S2F) + .and() + .withStates() + .parent(States2.S2) + .initial(States2.S3I) + .state(States2.S31) + .end(States2.S3F); + } + + } +// end::snippetP[] + +// tag::snippetQ[] + @Configuration + @EnableStateMachine + public static class Config11 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1, initialAction()) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + + @Bean + public Action initialAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + // do something initially + } + }; + } + + } +// end::snippetQ[] + +// tag::snippetR[] + @Configuration + @EnableStateMachine + public static class Config12 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States3.S1) + .state(States3.S2) + .and() + .withStates() + .parent(States3.S2) + .initial(States3.S2I) + .state(States3.S21) + .state(States3.S22) + .history(States3.SH, History.SHALLOW); + } + + } +// end::snippetR[] + +// tag::snippetS[] + @Configuration + @EnableStateMachine + public static class Config13 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.SI) + .choice(States.S1) + .end(States.SF) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withChoice() + .source(States.S1) + .first(States.S2, s2Guard()) + .then(States.S3, s3Guard()) + .last(States.S4); + } + + @Bean + public Guard s2Guard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return false; + } + }; + } + + @Bean + public Guard s3Guard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + return true; + } + }; + } + + } +// end::snippetS[] + +// tag::snippetT[] + @Configuration + @EnableStateMachine + public static class Config14 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States2.S1) + .fork(States2.S2) + .state(States2.S3) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S2I) + .state(States2.S21) + .state(States2.S22) + .end(States2.S2F) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S3I) + .state(States2.S31) + .state(States2.S32) + .end(States2.S3F); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withFork() + .source(States2.S2) + .target(States2.S22) + .target(States2.S32); + } + + } +// end::snippetT[] + +// tag::snippetU[] + @Configuration + @EnableStateMachine + public static class Config15 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States2.S1) + .state(States2.S3) + .join(States2.S4) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S2I) + .state(States2.S21) + .state(States2.S22) + .end(States2.S2F) + .and() + .withStates() + .parent(States2.S3) + .initial(States2.S3I) + .state(States2.S31) + .state(States2.S32) + .end(States2.S3F); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withJoin() + .source(States2.S2F) + .source(States2.S3F) + .target(States2.S5); + } + + } +// end::snippetU[] + } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/States2.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/States2.java new file mode 100644 index 00000000..68df59d2 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/States2.java @@ -0,0 +1,9 @@ +package org.springframework.statemachine.docs; + +//tag::snippetA[] +public enum States2 { + S1,S2,S3,S4,S5, + S2I,S21,S22,S2F, + S3I,S31,S32,S3F +} +//end::snippetA[] diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/States3.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/States3.java new file mode 100644 index 00000000..af5d2b21 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/States3.java @@ -0,0 +1,8 @@ +package org.springframework.statemachine.docs; + +//tag::snippetA[] +public enum States3 { + S1,S2,SH, + S2I,S21,S22,S2F +} +//end::snippetA[] diff --git a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java index 1b45b463..bb3f688e 100644 --- a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java @@ -23,12 +23,12 @@ import org.springframework.statemachine.guard.Guard; @Configuration public class Application { -//tag::snippetA[] @Configuration @EnableStateMachine static class StateMachineConfig extends EnumStateMachineConfigurerAdapter { +//tag::snippetAA[] @Override public void configure(StateMachineStateConfigurer states) throws Exception { @@ -62,7 +62,9 @@ public class Application { .state(States.AUTOMATIC, automaticAction(), null) .state(States.MANUAL); } +//end::snippetAA[] +//tag::snippetAB[] @Override public void configure(StateMachineTransitionConfigurer transitions) throws Exception { @@ -108,7 +110,9 @@ public class Application { .state(States.ERROR) .event(Events.FIX); } +//end::snippetAB[] +//tag::snippetAC[] @Bean public Guard tasksChoiceGuard() { return new Guard() { @@ -121,7 +125,9 @@ public class Application { } }; } +//end::snippetAC[] +//tag::snippetAD[] @Bean public Action automaticAction() { return new Action() { @@ -152,21 +158,25 @@ public class Application { } }; } +//end::snippetAD[] +//tag::snippetAE[] @Bean public Tasks tasks() { return new Tasks(); } +//end::snippetAE[] +//tag::snippetAF[] @Bean public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(5); return taskExecutor; } +//end::snippetAF[] } -//end::snippetA[] //tag::snippetB[] public static enum States { diff --git a/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java b/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java index 05f1ac87..b9a3cf71 100644 --- a/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java +++ b/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java @@ -41,24 +41,24 @@ public class Application { throws Exception { transitions .withExternal() - .source(States.WASHING) - .target(States.RINSING) + .source(States.WASHING).target(States.RINSING) .event(Events.RINSE) .and() .withExternal() - .source(States.RINSING) - .target(States.DRYING) + .source(States.RINSING).target(States.DRYING) .event(Events.DRY) .and() .withExternal() - .source(States.RUNNING) - .target(States.POWEROFF) + .source(States.RUNNING) .target(States.POWEROFF) .event(Events.CUTPOWER) .and() .withExternal() - .source(States.POWEROFF) - .target(States.HISTORY) - .event(Events.RESTOREPOWER); + .source(States.POWEROFF).target(States.HISTORY) + .event(Events.RESTOREPOWER) + .and() + .withExternal() + .source(States.RUNNING).target(States.END) + .event(Events.STOP); } //end::snippetAB[] @@ -74,7 +74,7 @@ public class Application { //tag::snippetC[] public static enum Events { - RINSE, DRY, + RINSE, DRY, STOP, RESTOREPOWER, CUTPOWER } //end::snippetC[] diff --git a/spring-statemachine-samples/washer/src/main/resources/statechartmodel.txt b/spring-statemachine-samples/washer/src/main/resources/statechartmodel.txt index 579812e5..b413cac9 100644 --- a/spring-statemachine-samples/washer/src/main/resources/statechartmodel.txt +++ b/spring-statemachine-samples/washer/src/main/resources/statechartmodel.txt @@ -1,27 +1,29 @@ -+----------------------------------------------------------------------------------+ -| | -+----------------------------------------------------------------------------------+ -| | -| +--------------------------------------------------------------------+ | -| *-->| RUNNING |-->X | -| +--------------------------------------------------------------------+ | -| | | | -| | +-------------+ +-------------+ +-------------+ | | -| | *-->| WASHING | RINSE | RINSING | DRY | DRYING | | | -| | | |------>| |------>| | | | -| | | | | | | | | | -| | +-------------+ +-------------+ +-------------+ | | -| | | | -| | H | | -| | ^ | | -| +------------|-------------------------------------------------------+ | -| | | | -| |RESTOREPOWER |CUTPOWER | -| | | | -| +-------------+ | | -| | POWEROFF | | | -| | |<----------+ | -| | | | -| +-------------+ | -| | -+----------------------------------------------------------------------------------+ ++--------------------------------------------------------------------------------------+ +| | ++--------------------------------------------------------------------------------------+ +| | +| +--------------------------------------------------------------------+ STOP | +| *-->| RUNNING |------>X | +| +--------------------------------------------------------------------+ | +| | | | +| | +-------------+ +-------------+ +-------------+ | | +| | *-->| WASHING | RINSE | RINSING | DRY | DRYING | | | +| | | |------>| |------>| | | | +| | | | | | | | | | +| | +-------------+ +-------------+ +-------------+ | | +| | | | +| | +-------+ | | +| | |HISTORY| | | +| | +-------+ | | +| | ^ | | +| +------------|-------------------------------------------------------+ | +| | | | +| |RESTOREPOWER |CUTPOWER | +| | | | +| +-------------+ | | +| | POWEROFF | | | +| | |<----------+ | +| | | | +| +-------------+ | +| | ++--------------------------------------------------------------------------------------+