diff --git a/settings.gradle b/settings.gradle index 706d4153..db02c02c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,7 @@ include 'spring-statemachine-samples:turnstile' include 'spring-statemachine-samples:showcase' include 'spring-statemachine-samples:cdplayer' include 'spring-statemachine-samples:tasks' +include 'spring-statemachine-samples:washer' rootProject.children.find { if (it.name == 'spring-statemachine-samples') { diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index e33c8853..17be701b 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -15,3 +15,7 @@ project('spring-statemachine-samples-cdplayer') { project('spring-statemachine-samples-tasks') { description = 'Spring State Machine Parallel Regions Sample' } + +project('spring-statemachine-samples-washer') { + description = 'Spring State Machine History State Sample' +} diff --git a/spring-statemachine-samples/washer/.gitignore b/spring-statemachine-samples/washer/.gitignore new file mode 100644 index 00000000..70e6e4b8 --- /dev/null +++ b/spring-statemachine-samples/washer/.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/washer/src/main/java/demo/washer/Application.java b/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java new file mode 100644 index 00000000..05f1ac87 --- /dev/null +++ b/spring-statemachine-samples/washer/src/main/java/demo/washer/Application.java @@ -0,0 +1,86 @@ +package demo.washer; + +import org.springframework.context.annotation.Configuration; +import org.springframework.shell.Bootstrap; +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.config.configurers.StateConfigurer.History; + +@Configuration +public class Application { + + @Configuration + @EnableStateMachine + static class StateMachineConfig + extends EnumStateMachineConfigurerAdapter { + +//tag::snippetAA[] + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.RUNNING) + .state(States.POWEROFF) + .end(States.END) + .and() + .withStates() + .parent(States.RUNNING) + .initial(States.WASHING) + .state(States.RINSING) + .state(States.DRYING) + .history(States.HISTORY, History.SHALLOW); + } +//end::snippetAA[] + +//tag::snippetAB[] + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.WASHING) + .target(States.RINSING) + .event(Events.RINSE) + .and() + .withExternal() + .source(States.RINSING) + .target(States.DRYING) + .event(Events.DRY) + .and() + .withExternal() + .source(States.RUNNING) + .target(States.POWEROFF) + .event(Events.CUTPOWER) + .and() + .withExternal() + .source(States.POWEROFF) + .target(States.HISTORY) + .event(Events.RESTOREPOWER); + } +//end::snippetAB[] + + } + +//tag::snippetB[] + public static enum States { + RUNNING, HISTORY, END, + WASHING, RINSING, DRYING, + POWEROFF + } +//end::snippetB[] + +//tag::snippetC[] + public static enum Events { + RINSE, DRY, + RESTOREPOWER, CUTPOWER + } +//end::snippetC[] + + public static void main(String[] args) throws Exception { + Bootstrap.main(args); + } + +} diff --git a/spring-statemachine-samples/washer/src/main/java/demo/washer/StateMachineCommands.java b/spring-statemachine-samples/washer/src/main/java/demo/washer/StateMachineCommands.java new file mode 100644 index 00000000..5a28ce26 --- /dev/null +++ b/spring-statemachine-samples/washer/src/main/java/demo/washer/StateMachineCommands.java @@ -0,0 +1,20 @@ +package demo.washer; + +import org.springframework.shell.core.annotation.CliCommand; +import org.springframework.shell.core.annotation.CliOption; +import org.springframework.stereotype.Component; + +import demo.AbstractStateMachineCommands; +import demo.washer.Application.Events; +import demo.washer.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/washer/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/washer/src/main/resources/META-INF/spring/spring-shell-plugin.xml new file mode 100644 index 00000000..1fc09f1f --- /dev/null +++ b/spring-statemachine-samples/washer/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/washer/src/main/resources/statechartmodel.txt b/spring-statemachine-samples/washer/src/main/resources/statechartmodel.txt new file mode 100644 index 00000000..579812e5 --- /dev/null +++ b/spring-statemachine-samples/washer/src/main/resources/statechartmodel.txt @@ -0,0 +1,27 @@ ++----------------------------------------------------------------------------------+ +| | ++----------------------------------------------------------------------------------+ +| | +| +--------------------------------------------------------------------+ | +| *-->| RUNNING |-->X | +| +--------------------------------------------------------------------+ | +| | | | +| | +-------------+ +-------------+ +-------------+ | | +| | *-->| WASHING | RINSE | RINSING | DRY | DRYING | | | +| | | |------>| |------>| | | | +| | | | | | | | | | +| | +-------------+ +-------------+ +-------------+ | | +| | | | +| | H | | +| | ^ | | +| +------------|-------------------------------------------------------+ | +| | | | +| |RESTOREPOWER |CUTPOWER | +| | | | +| +-------------+ | | +| | POWEROFF | | | +| | |<----------+ | +| | | | +| +-------------+ | +| | ++----------------------------------------------------------------------------------+ diff --git a/spring-statemachine-samples/washer/src/test/java/demo/washer/WasherTests.java b/spring-statemachine-samples/washer/src/test/java/demo/washer/WasherTests.java new file mode 100644 index 00000000..881fe8cc --- /dev/null +++ b/spring-statemachine-samples/washer/src/test/java/demo/washer/WasherTests.java @@ -0,0 +1,169 @@ +package demo.washer; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.statemachine.EnumStateMachine; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.listener.StateMachineListener; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; + +import demo.CommonConfiguration; +import demo.washer.Application.Events; +import demo.washer.Application.States; + +public class WasherTests { + + private AnnotationConfigApplicationContext context; + + private StateMachine machine; + + private TestListener listener; + + @Test + public void testInitialState() throws Exception { + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + listener.stateEnteredLatch.await(1, TimeUnit.SECONDS); + assertThat(machine.getState().getIds(), contains(States.RUNNING, States.WASHING)); + assertThat(listener.statesEntered.size(), is(2)); + assertThat(listener.statesEntered.get(0).getId(), is(States.RUNNING)); + assertThat(listener.statesEntered.get(1).getId(), is(States.WASHING)); + assertThat(listener.statesExited.size(), is(0)); + } + + @Test + public void testRinse() throws Exception { + listener.reset(1, 0, 0); + machine.sendEvent(Events.RINSE); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(machine.getState().getIds(), contains(States.RUNNING, States.RINSING)); + } + + @Test + public void testRinseCutPower() throws Exception { + listener.reset(1, 0, 0); + machine.sendEvent(Events.RINSE); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + + listener.reset(1, 0, 0); + machine.sendEvent(Events.CUTPOWER); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(machine.getState().getIds(), contains(States.POWEROFF)); + } + + @Test + public void testRinseCutRestorePower() throws Exception { + listener.reset(1, 0, 0); + machine.sendEvent(Events.RINSE); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + + listener.reset(1, 0, 0); + machine.sendEvent(Events.CUTPOWER); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + + listener.reset(1, 0, 0); + machine.sendEvent(Events.RESTOREPOWER); + listener.stateChangedLatch.await(1, TimeUnit.SECONDS); + assertThat(machine.getState().getIds(), contains(States.RUNNING, States.RINSING)); + } + + static class Config { + + @Autowired + private StateMachine machine; + + @Bean + public StateMachineListener stateMachineListener() { + TestListener listener = new TestListener(); + machine.addStateListener(listener); + return listener; + } + } + + static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile CountDownLatch stateEnteredLatch = new CountDownLatch(2); + volatile CountDownLatch stateExitedLatch = new CountDownLatch(0); + volatile CountDownLatch transitionLatch = new CountDownLatch(0); + volatile List> transitions = new ArrayList>(); + List> statesEntered = new ArrayList>(); + List> statesExited = new ArrayList>(); + volatile int transitionCount = 0; + + @Override + public void stateChanged(State from, State to) { + stateChangedLatch.countDown(); + } + + @Override + public void stateEntered(State state) { + statesEntered.add(state); + stateEnteredLatch.countDown(); + } + + @Override + public void stateExited(State state) { + statesExited.add(state); + stateExitedLatch.countDown(); + } + + @Override + public void transition(Transition transition) { + transitions.add(transition); + transitionLatch.countDown(); + transitionCount++; + } + + public void reset(int c1, int c2, int c3) { + reset(c1, c2, c3, 0); + } + + public void reset(int c1, int c2, int c3, int c4) { + stateChangedLatch = new CountDownLatch(c1); + stateEnteredLatch = new CountDownLatch(c2); + stateExitedLatch = new CountDownLatch(c3); + transitionLatch = new CountDownLatch(c4); + statesEntered.clear(); + statesExited.clear(); + transitionCount = 0; + transitions.clear(); + } + + } + + @SuppressWarnings("unchecked") + @Before + public void setup() { + context = new AnnotationConfigApplicationContext(); + context.register(CommonConfiguration.class, Application.class, Config.class); + context.refresh(); + machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + listener = context.getBean(TestListener.class); + machine.start(); + } + + @After + public void clean() { + machine.stop(); + context.close(); + context = null; + machine = null; + } + +}