From 706f1de23263545bb4d714358fc8f7863bea9db7 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sun, 26 Jul 2015 16:50:29 +0100 Subject: [PATCH] Update ref docs - First set of changes for doc updates for M3 - Relates #88 --- build.gradle | 3 + docs/src/reference/asciidoc/appendix.adoc | 8 + .../asciidoc/images/statechart10.png | Bin 0 -> 5952 bytes docs/src/reference/asciidoc/index.adoc | 1 + docs/src/reference/asciidoc/recipes.adoc | 130 ++++++++++++ docs/src/reference/asciidoc/sm-examples.adoc | 193 ++++++++++++++++++ docs/src/reference/asciidoc/sm.adoc | 156 +++++++++++++- .../docs/DocsConfigurationSampleTests.java | 122 ++++++++++- .../main/java/demo/persist/Application.java | 4 + .../src/main/java/demo/persist/Persist.java | 6 + .../main/java/demo/zookeeper/Application.java | 13 +- 11 files changed, 623 insertions(+), 13 deletions(-) create mode 100644 docs/src/reference/asciidoc/images/statechart10.png create mode 100644 docs/src/reference/asciidoc/recipes.adoc diff --git a/build.gradle b/build.gradle index 3f74fc14..66c9b0c2 100644 --- a/build.gradle +++ b/build.gradle @@ -246,12 +246,15 @@ configure(rootProject) { task copyDocsSamples(type: Copy) { from 'spring-statemachine-core/src/test/java/org/springframework/statemachine/docs' + from 'spring-statemachine-recipes/src/test/java/org/springframework/statemachine/recipes/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/' from 'spring-statemachine-samples/cdplayer/src/main/java/' + from 'spring-statemachine-samples/persist/src/main/java/' + from 'spring-statemachine-samples/zookeeper/src/main/java/' include '**/*.java' into 'docs/src/reference/asciidoc/samples' } diff --git a/docs/src/reference/asciidoc/appendix.adoc b/docs/src/reference/asciidoc/appendix.adoc index 9209f45d..8dbd8965 100644 --- a/docs/src/reference/asciidoc/appendix.adoc +++ b/docs/src/reference/asciidoc/appendix.adoc @@ -304,3 +304,11 @@ still working together in a sense. This is why orthogonal regions can combine together a multiple simultaneous states within a single state in a state machine. +[appendix] +[[appendices-zookeeper]] +== Distributed State Machine with Zookeeper +This appendix provides more detailed technical documentation about +using a Zookeeper with a Spring State Machine. + +tbd. + diff --git a/docs/src/reference/asciidoc/images/statechart10.png b/docs/src/reference/asciidoc/images/statechart10.png new file mode 100644 index 0000000000000000000000000000000000000000..1629ccc333c9e3124ff432d0773403416eb48d1a GIT binary patch literal 5952 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 literal 0 HcmV?d00001 diff --git a/docs/src/reference/asciidoc/index.adoc b/docs/src/reference/asciidoc/index.adoc index 0ea6b108..3db822bb 100644 --- a/docs/src/reference/asciidoc/index.adoc +++ b/docs/src/reference/asciidoc/index.adoc @@ -18,6 +18,7 @@ include::introduction.adoc[] [[springandsm]] include::sm.adoc[] +include::recipes.adoc[] include::sm-examples.adoc[] include::faq.adoc[] include::appendix.adoc[] diff --git a/docs/src/reference/asciidoc/recipes.adoc b/docs/src/reference/asciidoc/recipes.adoc new file mode 100644 index 00000000..254b568a --- /dev/null +++ b/docs/src/reference/asciidoc/recipes.adoc @@ -0,0 +1,130 @@ +[[statemachine-recipes]] += Recipes +This chapter contains documentation for existing built-in state +machine recipes. + +What exactly is a recipe? As Spring Statemachine is always going to be +a foundational framework meaning that its core will not have that much +higher level functionality or dependencies outside of a Spring +Framework. Correct usage of a state machine may be a little difficult +time to time and there's always some common use cases how state +machine can be used. Recipe modules are meant to provide a higher +level solutions to these common use cases and also provide examples +beyond samples how framework can be used. + +[NOTE] +==== +Recipes are a great way to make external contributions this Spring +Statemachine project. If you're not ready to contribute to the +framework core itself, a custom and common recipe is a great way to +share functionality among other users. +==== + +[[statemachine-recipes-persist]] +== Persist +Persist recipe is a simple utility which allows to use a single state +machine instance to persist and update a state of an arbitrary item in +a repository. + +Recipe's main class is `PersistStateMachineHandler` which assumes user +to do three different things: + +- An instance of a `StateMachine` needs to be used + with a `PersistStateMachineHandler`. States and Events are required + to be type of Strings. +- `PersistStateChangeListener` need to be registered with handler + order to react to persist request. +- Method `handleEventWithState` is used to orchestrate state changes. + +There is a sample demonstrating usage of this recipe at +<>. + +[[statemachine-recipes-tasks]] +== Tasks +Tasks recipe is a concept to execute DAG of `Runnable` instances using +a state machine. This recipe has been developed from ideas introduced +in sample <>. + +Generic concept of a state machine is shown below. In this state chart +everything under `TASKS` just shows a generic concept of how a single +task is executed. Because this recipe allows to register deep +hierarcical DAG of tasks, meaning a real state chart would be deep +nested collection of sub-states and regions, there's no need to be +more presise. + +For example if you have only two registered tasks, below state chart +would be correct with `TASK_id` replaced with `TASK_1` and `TASK_2` if +registered tasks ids are `1` and `2`. + +image::images/statechart9.png[width=500] + +Executing a `Runnable` may result an error and especially if a complex +DAG of tasks is involved it is desirable that there is a way to handle +tasks execution errors and then having a way to continue execution +without executing already successfully executed tasks. Addition to +this it would be nice if some execution errors can be handled +automatically and as a last fallback, if error can't be handled +automatically, state maching is put into a state where user can handle +errors manually. + +`TasksHandler` contains a builder method to configure handler instance +and follows a simple builder patter. This builder can be used to +register `Runnable` tasks, `TasksListener` instances, define +`StateMachinePersist` hook, and setup custom `TaskExecutor` instance. + +Now lets take a simple `Runnable` just doing a simple sleep as shown +below. This is a base of all examples in this chapter. + +[source,java,indent=0] +---- +include::samples/DocsTasksSampleTests.java[tags=snippetAA] +---- + +To execute multiple `sleepRunnable` tasks just register tasks and +execute `runTasks()` method from `TasksHandler`. + +[source,java,indent=0] +---- +include::samples/DocsTasksSampleTests.java[tags=snippetB] +---- + +Order to listen what is happening with a task execution an instance of +a `TasksListener` can be registered with a `TasksHandler`. Recipe +provides an adapter `TasksListenerAdapter` if you dont' want to +implement a full interface. Listener provides a various hooks to +listen tasks execution events. + +[source,java,indent=0] +---- +include::samples/DocsTasksSampleTests.java[tags=snippetAB] +---- + +Listeners can be either registered via a builder or directly with a +`TasksHandler` as shown above. + +[source,java,indent=0] +---- +include::samples/DocsTasksSampleTests.java[tags=snippetC] +---- + +Above sample show how to create a deep nested DAG of tasks. Every task +needs to have an unique identifier and optionally as task can be +defined to be a sub-task. Effectively this will create a DAG of tasks. + +[source,java,indent=0] +---- +include::samples/DocsTasksSampleTests.java[tags=snippetD] +---- + +When error happens and a state machine running these tasks goes into a +`ERROR` state, user can call handler methods `fixCurrentProblems` to +reset current state of tasks kept in a state machine extended state +variables. Handler method `continueFromError` can then be used to +instruct state machine to transition from `ERROR` state back to +`READY` state where tasks can be executed again. + +[source,java,indent=0] +---- +include::samples/DocsTasksSampleTests.java[tags=snippetE] +---- + diff --git a/docs/src/reference/asciidoc/sm-examples.adoc b/docs/src/reference/asciidoc/sm-examples.adoc index 930495fe..71f47c57 100644 --- a/docs/src/reference/asciidoc/sm-examples.adoc +++ b/docs/src/reference/asciidoc/sm-examples.adoc @@ -20,6 +20,7 @@ Every sample is located in its own directory under spring-shell and you will find usual boot fat jars under every sample projects `build/libs` directory. +[[statemachine-examples-turnstile]] == Turnstile Turnstile is a simple device which gives you an access if payment is @@ -442,6 +443,7 @@ What happened in above run: * We print lcd status and request next track. * We stop playing. +[[statemachine-examples-tasks]] == Tasks Tasks is a sample demonstrating a parallel task handling within a @@ -701,3 +703,194 @@ What happened in above run: * State is restored via HISTORY state which takes state machine back to its previous known state. +[[statemachine-examples-persist]] +== Persist +Persist is a sample using recipe <> to +demonstate how a database entry update logic can be controlled by a +state machine. + +The state machine logic and configuration is shown above: + +image::images/statechart10.png[width=500] + +.StateMachine Config +[source,java,indent=0] +---- +include::samples/demo/persist/Application.java[tags=snippetA] +---- + +`PersistStateMachineHandler` can be created using a below config: + +.Handler Config +[source,java,indent=0] +---- +include::samples/demo/persist/Application.java[tags=snippetB] +---- + +Order class used with this sample is shown below: + +.Order Class +[source,java,indent=0] +---- +include::samples/demo/persist/Application.java[tags=snippetC] +---- + +Now let's see how this example works. + +[source,text] +---- +sm>persist db +Order [id=1, state=PLACED] +Order [id=2, state=PROCESSING] +Order [id=3, state=SENT] +Order [id=4, state=DELIVERED] + +sm>persist process 1 +Exit state PLACED +Entry state PROCESSING + +sm>persist db +Order [id=2, state=PROCESSING] +Order [id=3, state=SENT] +Order [id=4, state=DELIVERED] +Order [id=1, state=PROCESSING] + +sm>persist deliver 3 +Exit state SENT +Entry state DELIVERED + +sm>persist db +Order [id=2, state=PROCESSING] +Order [id=4, state=DELIVERED] +Order [id=1, state=PROCESSING] +Order [id=3, state=DELIVERED] +---- + +What happened in above run: + +* We listed rows from an existing embedded database which is already + populated with sample data. +* We request to update order `1` into `PROCESSING` state. +* We list db entries again and see that state has been changed from + `PLACED` into a `PROCESSING`. +* We do update for order `3` to update state from `SENT` into + `DELIVERED`. + +[NOTE] +==== +If you're wondering where is the database because there are literally no +signs of it in a sample code. Sample is based on Spring Boot and +because necessary classes are in a classpath, embedded `HSQL` instance +is created automatically. + +Spring Boot will even create an instance of `JdbcTemplate` which you +can just autowire like how it's done in `Persist.java`. + +[source,java,indent=0] +---- +include::samples/demo/persist/Persist.java[tags=snippetA] +---- +==== + +Finally we need to handle state changes: + +[source,java,indent=0] +---- +include::samples/demo/persist/Persist.java[tags=snippetB] +---- + +And use a `PersistStateChangeListener` to update database: + +[source,java,indent=0] +---- +include::samples/demo/persist/Persist.java[tags=snippetC] +---- + +[[statemachine-examples-zookeeper]] +== Zookeeper +Zookeeper is a distributed version from sample +<>. + +[NOTE] +==== +This sample needs and external `Zookeeper` instance accessible from +`localhost` with default port and settings. +==== + +Configuration of this sample is almost same as `turnstile` sample. We +only add configuration for distributed state machine where we +configure `StateMachineEnsemble`. + +[source,java,indent=0] +---- +include::samples/demo/zookeeper/Application.java[tags=snippetA] +---- + +Actual `StateMachineEnsemble` needs to be created as bean together +with `CuratorFramework` client. + +[source,java,indent=0] +---- +include::samples/demo/zookeeper/Application.java[tags=snippetB] +---- + +Lets go through a simple example where two different shell instances are +started with command `java -jar +spring-statemachine-samples-zookeeper-1.0.0.BUILD-SNAPSHOT.jar`. + +First open first shell instance(do not start second instance yet). +When state machine is started it will end up into its initial state +`LOCKED`. Then send event `COIN` to transit into `UNLOCKED` state. + +.Shell1 +[source,text] +---- +sm>sm start +Entry state LOCKED +State machine started + +sm>sm event COIN +Exit state LOCKED +Entry state UNLOCKED +Event COIN send + +sm>sm state +UNLOCKED +---- + +Open second shell instance and start a state machine. You should see +that distributed state `UNLOCKED` is entered instead of default +initial state `LOCKED`. + +.Shell2 +[source,text] +---- +sm>sm start +State machine started + +sm>sm state +UNLOCKED +---- + +Then from either of a shells(we use second instance here) send event +`PUSH` to transit from `UNLOCKED` into `LOCKED` state. + +.Shell2 +[source,text] +---- +sm>sm event PUSH +Exit state UNLOCKED +Entry state LOCKED +Event PUSH send +---- + +In other shell you should see state getting changed automatically +based on distributed state kept in Zookeeper. + +.Shell1 +[source,text] +---- +sm>Exit state UNLOCKED +Entry state LOCKED +---- + diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index 683fd8db..d54d2912 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -31,6 +31,7 @@ simply made code snippets less verbose by leaving other needed parts away. ==== +[[statemachine-config]] === 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 @@ -208,6 +209,7 @@ states from a regions. include::samples/DocsConfigurationSampleTests.java[tags=snippetU] ---- +[[statemachine-config-commonsettings]] === Configuring Common Settings Some of a common state machine configuration can be set via a `ConfigurationConfigurer`. This allows to set `BeanFactory`, @@ -216,7 +218,7 @@ and register `StateMachineListener` instances. [source,java,indent=0] ---- -include::samples/DocsConfigurationSampleTests.java[tags=snippetY] +include::samples/DocsConfigurationSampleTests.java[tags=snippetYA] ---- State machine `autoStartup` flag is disabled by default because all @@ -236,6 +238,18 @@ start/stop events. Naturally it is not possible to listen a state machine start events if `autoStartup` is enabled unless listener can be registered during a configuration phase. +`DistributedStateMachine` is configured via `withDistributed()` which +allows to set a `StateMachineEnsemble` which if exists automatically +wraps created `StateMachine` with `DistributedStateMachine` and +enables distributed mode. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetYB] +---- + +More about distributed states, refer to section <>. + [[sm-factories]] == State Machine Factories There are use cases when state machine needs to be created dynamically @@ -523,3 +537,143 @@ include::samples/DocsConfigurationSampleTests.java[tags=snippetK] In your own bean you can then use this _@StatesOnTransition_ as is and use type safe `source` and `target`. +[[sm-accessor]] +== State Machine Accessor +`StateMachine` is a main interface to communicate with a state machine +itself. Time to time there is a need to get more dynamical and +programmatic access to internal structures of a state machine and its +nested machines and regions. For these use cases a `StateMachine` is +exposing a functional interface `StateMachineAccessor` which provides +an interface to get access to individual `StateMachine` and +`Region` instances. + +`StateMachineFunction` is a simple functional interface which allows +to apply `StateMachineAccess` interface into a state machine. With +jdk7 these will create a little verbose code but with jdk8 lambdas +things look relatively non-verbose. + +Method `doWithAllRegions` gives access to all `Region` instances in +a state machine. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetZA] +---- + +Method `doWithRegion` gives access to single `Region` instance in a +state machine. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetZB] +---- + +Method `withAllRegions` gives access to all `Region` instances in +a state machine. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetZC] +---- + +Method `withRegion` gives access to single `Region` instance in a +state machine. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetZD] +---- + +[[sm-interceptor]] +== State Machine Interceptor +Instead of using a `StateMachineListener` interface one option is to +use a `StateMachineInterceptor`. One conceptual difference is that an +interceptor can be used to intercept and stop a current state +change or transition logic. Instead of implementing full interface, +adapter class `StateMachineInterceptorAdapter` can be used to override +default no-op methods. + +[NOTE] +==== +There is one recipe <> and one sample +<> which are related to use of an +interceptor. +==== + +Interceptor can be registered via `StateMachineAccessor`. Concept of +an interceptor is relatively deep internal feature and thus is not +exposed directly via `StateMachine` interface. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests.java[tags=snippetZH] +---- + + +[[sm-persist]] +== Persisting State Machine +Traditionally an instance of a state machine is used as is within a +running program. More dynamic behaviour is possible to achieve via +dynamic builders and factories which allows state machine +instantiation on-demand. Building an instance of a state machine is +relatively heavy operation so if there is a need to i.e. handle +arbitrary state change in a database using a state machine we need to +find a better and faster way to do it. + +Persist feature allows user to save a state of a state machine itself +into an external repository and later reset a state machine based of +serialized state. For example if you have a database table keeping +orders it would be way too expensive to update order state via a state +machine if a new instance would need to be build for every change. +Persist feature allows you to reset a state machine state without +instantiating a new state machine instance. + +[NOTE] +==== +There is one recipe <> and one sample +<> which provides more info about +persisting states. +==== + +While it is possible to build a custom persistence feature using a +`StateMachineListener` it has one conceptual problem. When listener +notifies a change of state, state change has already happened. If a +custom persistent method within a listener fails to update serialized +state in an external repository, state in a state machine and state in +an external repository are then in inconsistent state. + +State machine interceptor can be used instead of where attempt to save +serialized state into an external storage is done during the a state +change within a state machine. If this interceptor callback fails, +state change attempt will be halted and instead of ending into an +inconsistent state, user can then handle this error manually. Using +the interceptors are discussed in <>. + +[[sm-distributed]] +== Using Distributed States +Distributed state is probably one of a most compicated concepts of a +Spring State Machine. What exactly is a distributed state? A state +within a single state machine is naturally really simple to understand +but when there is a need to introduce a shared distributed state +thoughout a state machines, things will get a little complicated. + +For configuration support see section +<> and actual usage example see +sample <>. + +`Distributed State Machine` is implemented via a +`DistributedStateMachine` class which simply wraps an actual instance +of a `StateMachine`. `DistributedStateMachine` intercepts +communication with a `StateMachine` instance and works with +distributed state abstractions handled via interface +`StateMachineEnsemble`. Depending on an actual implementation +`StateMachinePersist` interface may also be used to serialize a +`StateMachineContext` which contains enought information to reset a +`StateMachine`. + +While `Distributed State Machine` is implemented via an abstraction, +only one implementation currently exists based on `Zookeeper`. + +Current technical documentation of a `Zookeeker` based distributed +state machine can be found from an appendice <>. + 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 8c1a77fc..eded8c56 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 @@ -38,6 +38,8 @@ import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; import org.springframework.statemachine.AbstractStateMachineTests; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.access.StateMachineAccess; +import org.springframework.statemachine.access.StateMachineFunction; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.action.SpelExpressionAction; import org.springframework.statemachine.annotation.OnTransition; @@ -46,17 +48,19 @@ import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnableStateMachineFactory; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.StateMachineBuilder; +import org.springframework.statemachine.config.StateMachineBuilder.Builder; import org.springframework.statemachine.config.StateMachineConfigurerAdapter; import org.springframework.statemachine.config.StateMachineFactory; -import org.springframework.statemachine.config.StateMachineBuilder.Builder; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; 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.ensemble.StateMachineEnsemble; import org.springframework.statemachine.event.StateMachineEvent; import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.state.State; +import org.springframework.statemachine.support.StateMachineInterceptor; import org.springframework.statemachine.transition.Transition; /** @@ -735,7 +739,7 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { } -// tag::snippetY[] +// tag::snippetYA[] @Configuration @EnableStateMachine public static class Config17 @@ -754,6 +758,118 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { } } -// end::snippetY[] +// end::snippetYA[] + +// tag::snippetYB[] + @Configuration + @EnableStateMachine + public static class Config18 + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withDistributed() + .ensemble(stateMachineEnsemble()); + } + + @Bean + public StateMachineEnsemble stateMachineEnsemble() + throws Exception { + // naturally not null but should return ensemble instance + return null; + } + + } +// end::snippetYB[] + + public static class AccessorSamples { + + StateMachine stateMachine = null; + + void s1() { +// tag::snippetZA[] + stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess function) { + function.setRelay(stateMachine); + } + }); + + stateMachine.getStateMachineAccessor() + .doWithAllRegions(access -> access.setRelay(stateMachine)); +// end::snippetZA[] + } + + void s2() { +// tag::snippetZB[] + stateMachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess function) { + function.setRelay(stateMachine); + } + }); + + stateMachine.getStateMachineAccessor() + .doWithRegion(access -> access.setRelay(stateMachine)); +// end::snippetZB[] + } + + void s3() { +// tag::snippetZC[] + for (StateMachineAccess access : stateMachine.getStateMachineAccessor().withAllRegions()) { + access.setRelay(stateMachine); + } + + stateMachine.getStateMachineAccessor().withAllRegions() + .stream().forEach(access -> access.setRelay(stateMachine)); +// end::snippetZC[] + } + + void s4() { +// tag::snippetZD[] + stateMachine.getStateMachineAccessor() + .withRegion().setRelay(stateMachine); +// end::snippetZD[] + } + + } + + public static class InterceptorSamples { + + StateMachine stateMachine = null; + + void s1() { +// tag::snippetZH[] + stateMachine.getStateMachineAccessor() + .withRegion().addStateMachineInterceptor(new StateMachineInterceptor() { + + @Override + public StateContext preTransition(StateContext stateContext) { + return stateContext; + } + + @Override + public void preStateChange(State state, Message message, + Transition transition, StateMachine stateMachine) { + } + + @Override + public StateContext postTransition(StateContext stateContext) { + return stateContext; + } + + @Override + public void postStateChange(State state, Message message, + Transition transition, StateMachine stateMachine) { + } + }); +// end::snippetZH[] + } + + } } diff --git a/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java b/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java index 862b9d56..13f8ca7b 100644 --- a/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java @@ -67,6 +67,7 @@ public class Application { } //end::snippetA[] +//tag::snippetB[] @Configuration static class PersistHandlerConfig { @@ -84,7 +85,9 @@ public class Application { } } +//end::snippetB[] +//tag::snippetC[] public static class Order { int id; String state; @@ -100,6 +103,7 @@ public class Application { } } +//end::snippetC[] public static void main(String[] args) throws Exception { Bootstrap.main(args); diff --git a/spring-statemachine-samples/persist/src/main/java/demo/persist/Persist.java b/spring-statemachine-samples/persist/src/main/java/demo/persist/Persist.java index dca88c3f..21554cfd 100644 --- a/spring-statemachine-samples/persist/src/main/java/demo/persist/Persist.java +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/Persist.java @@ -36,8 +36,10 @@ public class Persist { private final PersistStateMachineHandler handler; +//tag::snippetA[] @Autowired private JdbcTemplate jdbcTemplate; +//end::snippetA[] private final PersistStateChangeListener listener = new LocalPersistStateChangeListener(); @@ -62,6 +64,7 @@ public class Persist { return buf.toString(); } +//tag::snippetB[] public void change(int order, String event) { Order o = jdbcTemplate.queryForObject("select id, state from orders where id = ?", new Object[]{order}, new RowMapper() { public Order mapRow(ResultSet rs, int rowNum) throws SQLException { @@ -70,7 +73,9 @@ public class Persist { }); handler.handleEventWithState(MessageBuilder.withPayload(event).setHeader("order", order).build(), o.state); } +//end::snippetB[] +//tag::snippetC[] private class LocalPersistStateChangeListener implements PersistStateChangeListener { @Override @@ -82,5 +87,6 @@ public class Persist { } } } +//end::snippetC[] } diff --git a/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/Application.java b/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/Application.java index a6a2ee12..39cbe3c0 100644 --- a/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/Application.java +++ b/spring-statemachine-samples/zookeeper/src/main/java/demo/zookeeper/Application.java @@ -32,18 +32,19 @@ import org.springframework.statemachine.zookeeper.ZookeeperStateMachineEnsemble; @Configuration public class Application { -//tag::snippetA[] @Configuration @EnableStateMachine static class StateMachineConfig extends StateMachineConfigurerAdapter { +//tag::snippetA[] @Override public void configure(StateMachineConfigurationConfigurer config) throws Exception { config .withDistributed() .ensemble(stateMachineEnsemble()); } +//end::snippetA[] @Override public void configure(StateMachineStateConfigurer states) @@ -69,6 +70,7 @@ public class Application { .event("PUSH"); } +//tag::snippetB[] @Bean public StateMachineEnsemble stateMachineEnsemble() throws Exception { return new ZookeeperStateMachineEnsemble(curatorClient(), "/foo"); @@ -79,27 +81,20 @@ public class Application { CuratorFramework client = CuratorFrameworkFactory.builder().defaultData(new byte[0]) .retryPolicy(new ExponentialBackoffRetry(1000, 3)) .connectString("localhost:2181").build(); - // for testing we start it here, thought initiator - // is trying to start it if not already done client.start(); return client; } - +//end::snippetB[] } -//end::snippetA[] -//tag::snippetB[] public static enum States { LOCKED, UNLOCKED } -//end::snippetB[] -//tag::snippetC[] public static enum Events { COIN, PUSH } -//end::snippetC[] public static void main(String[] args) throws Exception { Bootstrap.main(args);