diff --git a/build.gradle b/build.gradle index 86c3f855..16ad87ad 100644 --- a/build.gradle +++ b/build.gradle @@ -115,6 +115,10 @@ configure(sampleProjects()) { apply plugin: 'spring-boot' dependencies { compile project(":spring-statemachine-samples-common") + testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" + testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" + testCompile "junit:junit:$junitVersion" } } @@ -176,6 +180,7 @@ configure(rootProject) { from 'spring-statemachine-core/src/test/java/org/springframework/statemachine/docs' from 'spring-statemachine-samples/src/main/java/' from 'spring-statemachine-samples/turnstile/src/main/java/' + from 'spring-statemachine-samples/showcase/src/main/java/' include '**/*.java' into 'docs/src/reference/asciidoc/samples' } diff --git a/docs/src/reference/asciidoc/images/statechart2.png b/docs/src/reference/asciidoc/images/statechart2.png new file mode 100644 index 00000000..76f06e44 Binary files /dev/null and b/docs/src/reference/asciidoc/images/statechart2.png differ diff --git a/docs/src/reference/asciidoc/sm-examples.adoc b/docs/src/reference/asciidoc/sm-examples.adoc index 831e7681..ca8c1078 100644 --- a/docs/src/reference/asciidoc/sm-examples.adoc +++ b/docs/src/reference/asciidoc/sm-examples.adoc @@ -75,3 +75,39 @@ State changed to LOCKED Event PUSH send ---- +== Showcase + +Showcase is a complex state machine showing all possible transition +topologies up to four levels of state nesting. + +image::images/statechart2.png[] + +.States +[source,java,indent=0] +---- +include::samples/demo/showcase/Application.java[tags=snippetB] +---- + +.Events +[source,java,indent=0] +---- +include::samples/demo/showcase/Application.java[tags=snippetC] +---- + +.Configuration +[source,java,indent=0] +---- +include::samples/demo/showcase/Application.java[tags=snippetA] +---- + +.Guard +[source,java,indent=0] +---- +include::samples/demo/showcase/Application.java[tags=snippetD] +---- + +.Action +[source,java,indent=0] +---- +include::samples/demo/showcase/Application.java[tags=snippetE] +---- diff --git a/settings.gradle b/settings.gradle index 009551c7..0ebfe7cb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,7 @@ include 'spring-statemachine-core' include 'spring-statemachine-samples' include 'spring-statemachine-samples:turnstile' +include 'spring-statemachine-samples:showcase' rootProject.children.find { if (it.name == 'spring-statemachine-samples') { diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index f7902797..9877be75 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -4,3 +4,6 @@ project('spring-statemachine-samples-turnstile') { description = 'Spring State Machine Turnstile Sample' } +project('spring-statemachine-samples-showcase') { + description = 'Spring State Machine Showcase Sample' +} diff --git a/spring-statemachine-samples/showcase/.gitignore b/spring-statemachine-samples/showcase/.gitignore new file mode 100644 index 00000000..70e6e4b8 --- /dev/null +++ b/spring-statemachine-samples/showcase/.gitignore @@ -0,0 +1,19 @@ +.gradle +bin +build +.settings +.classpath +.springBeans +.project +*.iml +*.ipr +*.iws +metastore_db +/samples/pig-scripting/src/main/resources/ml-100k.zip +/samples/pig-scripting/src/main/resources/ml-100k/u.data +/src/test/resources/s3.properties +/.idea/ +.DS_Store +/out/ +target +*.log diff --git a/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java new file mode 100644 index 00000000..86d24b07 --- /dev/null +++ b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/Application.java @@ -0,0 +1,154 @@ +package demo.showcase; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.shell.Bootstrap; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.guard.Guard; + +@Configuration +public class Application { + +//tag::snippetA[] + @Configuration + @EnableStateMachine + static class StateMachineConfig + extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S0) + .state(States.S0) + .and() + .withStates() + .parent(States.S0) + .initial(States.S1) + .state(States.S1) + .and() + .withStates() + .parent(States.S1) + .initial(States.S11) + .state(States.S11) + .and() + .withStates() + .parent(States.S0) + .state(States.S2) + .and() + .withStates() + .parent(States.S2) + .initial(States.S21) + .state(States.S21) + .and() + .withStates() + .parent(States.S21) + .initial(States.S211) + .state(States.S211); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S1).target(States.S1).event(Events.A) + .action(fooAction()) + .and() + .withExternal() + .source(States.S1).target(States.S11).event(Events.B) + .and() + .withExternal() + .source(States.S21).target(States.S211).event(Events.B) + .and() + .withExternal() + .source(States.S1).target(States.S2).event(Events.C) + .and() + .withExternal() + .source(States.S2).target(States.S1).event(Events.C) + .and() + .withExternal() + .source(States.S1).target(States.S0).event(Events.D) + .and() + .withExternal() + .source(States.S211).target(States.S21).event(Events.D) + .and() + .withExternal() + .source(States.S0).target(States.S211).event(Events.E) + .and() + .withExternal() + .source(States.S1).target(States.S211).event(Events.F) + .and() + .withExternal() + .source(States.S2).target(States.S11).event(Events.F) + .and() + .withExternal() + .source(States.S11).target(States.S211).event(Events.G) + .and() + .withExternal() + .source(States.S211).target(States.S0).event(Events.G) + .and() + .withExternal() + .source(States.S21).target(States.S21).event(Events.H) + .guard(fooGuard()); + + } + + @Bean + public FooGuard fooGuard() { + return new FooGuard(); + } + + @Bean + public FooAction fooAction() { + return new FooAction(); + } + + } +//end::snippetA[] + +//tag::snippetB[] + public static enum States { + S0, S1, S11, S2, S21, S211 + } +//end::snippetB[] + +//tag::snippetC[] + public static enum Events { + A, B, C, D, E, F, G, H + } +//end::snippetC[] + +// tag::snippetD[] + private static class FooAction implements Action { + + @Override + public void execute(StateContext context) { + context.getExtendedState().getVariables().put("foo", 1); + } + } + +// end::snippetD[] + +// tag::snippetE[] + private static class FooGuard implements Guard { + + @Override + public boolean evaluate(StateContext context) { + Object foo = context.getExtendedState().getVariables().get("foo"); + return !(foo == null || !foo.equals(1)); + } + } +// end::snippetE[] + + public static void main(String[] args) throws Exception { + Bootstrap.main(args); + } + +} diff --git a/spring-statemachine-samples/showcase/src/main/java/demo/showcase/StateMachineCommands.java b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/StateMachineCommands.java new file mode 100644 index 00000000..f8adb7fe --- /dev/null +++ b/spring-statemachine-samples/showcase/src/main/java/demo/showcase/StateMachineCommands.java @@ -0,0 +1,20 @@ +package demo.showcase; + +import org.springframework.shell.core.annotation.CliCommand; +import org.springframework.shell.core.annotation.CliOption; +import org.springframework.stereotype.Component; + +import demo.AbstractStateMachineCommands; +import demo.showcase.Application.Events; +import demo.showcase.Application.States; + +@Component +public class StateMachineCommands extends AbstractStateMachineCommands { + + @CliCommand(value = "sm event", help = "Sends an event to a state machine") + public String event(@CliOption(key = { "", "event" }, mandatory = true, help = "The event") final Events event) { + getStateMachine().sendEvent(event); + return "Event " + event + " send"; + } + +} \ No newline at end of file diff --git a/spring-statemachine-samples/showcase/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/showcase/src/main/resources/META-INF/spring/spring-shell-plugin.xml new file mode 100644 index 00000000..1fc09f1f --- /dev/null +++ b/spring-statemachine-samples/showcase/src/main/resources/META-INF/spring/spring-shell-plugin.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/spring-statemachine-samples/showcase/src/main/resources/statechartmodel.txt b/spring-statemachine-samples/showcase/src/main/resources/statechartmodel.txt new file mode 100644 index 00000000..c0da1297 --- /dev/null +++ b/spring-statemachine-samples/showcase/src/main/resources/statechartmodel.txt @@ -0,0 +1,33 @@ ++---------------------------------------------------------------------------------------------+ +| S0 | ++---------------------------------------------------------------------------------------------+ +| entry/ | +| exit/ | +| +-------------------------+ +--------------------------------------------+ | +| *-->| S1 | | S2 | | +| +-------------------------+ C +--------------------------------------------+ | +| | entry/ | | entry/ H | | +| D | exit/ |----->| exit/ +-------------------+ | | +|<<---------| | | | H[foo.equals(1)]; | | | +| | +---------------+ | C | +------------------------------+ | | | +| | *-->| S11 | |<-----| *-->| S21 |<--+ | | +| | +---------------+ | | +------------------------------+ | | +| +--| | entry/ | | F | | entry/ | | | +| A| | | exit/ |<---------| | exit/ | | | +| +->| | | | | | +--------------+ | | | +| A[foo 1]; | B | | | | | *-->| s211 | | | | +| |---->| | | | F | +--------------+ | | | +| | | | |-------------------->| entry/ | | G | | +| | | | | | G | | exit/ |----------------->| +| | | |------------------------>| | | | | +| | | | | | | B | | | E | | +| | | | | | |------>| |<-----------------| +| | | | | | | | | | | | +| | | | | | | D | | | | | +| | | | | | |<------| | | | | +| | | | | | | +--------------+ | | | +| | +---------------+ | | +------------------------------+ | | +| | | | | | +| +-------------------------+ +--------------------------------------------+ | +| | ++---------------------------------------------------------------------------------------------+ diff --git a/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java b/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java new file mode 100644 index 00000000..e2a53907 --- /dev/null +++ b/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java @@ -0,0 +1,41 @@ +package demo.showcase; + +import org.junit.After; +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.statemachine.EnumStateMachine; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; + +import demo.CommonConfiguration; +import demo.showcase.Application.Events; +import demo.showcase.Application.States; + +public class ShowcaseTests { + + private AnnotationConfigApplicationContext context; + + @SuppressWarnings("resource") + @Test + public void testApplication() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(CommonConfiguration.class, Application.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + + machine.start(); + + machine.sendEvent(Events.A); + + } + + @After + public void clean() { + if (context != null) { + context.close(); + context = null; + } + } + +}