Update ref docs

- First set of changes for doc updates for M3
- Relates #88
This commit is contained in:
Janne Valkealahti
2015-07-26 16:50:29 +01:00
parent e92a6c5a09
commit 706f1de232
11 changed files with 623 additions and 13 deletions

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -18,6 +18,7 @@ include::introduction.adoc[]
[[springandsm]]
include::sm.adoc[]
include::recipes.adoc[]
include::sm-examples.adoc[]
include::faq.adoc[]
include::appendix.adoc[]

View File

@@ -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<String, String>` 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-examples-persist>>.
[[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 <<statemachine-examples-tasks>>.
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]
----

View File

@@ -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 <<statemachine-recipes-persist>> 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
<<statemachine-examples-turnstile>>.
[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
----

View File

@@ -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-distributed>>.
[[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 <<statemachine-recipes-persist>> and one sample
<<statemachine-examples-persist>> 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 <<statemachine-recipes-persist>> and one sample
<<statemachine-examples-persist>> 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-interceptor>>.
[[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
<<statemachine-config-commonsettings>> and actual usage example see
sample <<statemachine-examples-zookeeper>>.
`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 <<appendices-zookeeper>>.

View File

@@ -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<States, Events> {
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
throws Exception {
config
.withDistributed()
.ensemble(stateMachineEnsemble());
}
@Bean
public StateMachineEnsemble<States, Events> stateMachineEnsemble()
throws Exception {
// naturally not null but should return ensemble instance
return null;
}
}
// end::snippetYB[]
public static class AccessorSamples {
StateMachine<String, String> stateMachine = null;
void s1() {
// tag::snippetZA[]
stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<String,String>>() {
@Override
public void apply(StateMachineAccess<String, String> function) {
function.setRelay(stateMachine);
}
});
stateMachine.getStateMachineAccessor()
.doWithAllRegions(access -> access.setRelay(stateMachine));
// end::snippetZA[]
}
void s2() {
// tag::snippetZB[]
stateMachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<String,String>>() {
@Override
public void apply(StateMachineAccess<String, String> function) {
function.setRelay(stateMachine);
}
});
stateMachine.getStateMachineAccessor()
.doWithRegion(access -> access.setRelay(stateMachine));
// end::snippetZB[]
}
void s3() {
// tag::snippetZC[]
for (StateMachineAccess<String, String> 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<String, String> stateMachine = null;
void s1() {
// tag::snippetZH[]
stateMachine.getStateMachineAccessor()
.withRegion().addStateMachineInterceptor(new StateMachineInterceptor<String, String>() {
@Override
public StateContext<String, String> preTransition(StateContext<String, String> stateContext) {
return stateContext;
}
@Override
public void preStateChange(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
}
@Override
public StateContext<String, String> postTransition(StateContext<String, String> stateContext) {
return stateContext;
}
@Override
public void postStateChange(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
}
});
// end::snippetZH[]
}
}
}

View File

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

View File

@@ -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<Order>() {
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[]
}

View File

@@ -32,18 +32,19 @@ import org.springframework.statemachine.zookeeper.ZookeeperStateMachineEnsemble;
@Configuration
public class Application {
//tag::snippetA[]
@Configuration
@EnableStateMachine
static class StateMachineConfig
extends StateMachineConfigurerAdapter<String, String> {
//tag::snippetA[]
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config
.withDistributed()
.ensemble(stateMachineEnsemble());
}
//end::snippetA[]
@Override
public void configure(StateMachineStateConfigurer<String, String> states)
@@ -69,6 +70,7 @@ public class Application {
.event("PUSH");
}
//tag::snippetB[]
@Bean
public StateMachineEnsemble<String, String> stateMachineEnsemble() throws Exception {
return new ZookeeperStateMachineEnsemble<String, String>(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);