From cc4794d014c5f8b3cdfba8225e9c6c273753c680 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sun, 17 May 2015 16:04:52 +0100 Subject: [PATCH] Add new tasks sample --- build.gradle | 1 + settings.gradle | 1 + spring-statemachine-samples/build.gradle | 3 + spring-statemachine-samples/tasks/.gitignore | 19 ++ .../src/main/java/demo/tasks/Application.java | 193 ++++++++++++++++++ .../java/demo/tasks/StateMachineCommands.java | 20 ++ .../tasks/src/main/java/demo/tasks/Tasks.java | 95 +++++++++ .../main/java/demo/tasks/TasksCommands.java | 35 ++++ .../META-INF/spring/spring-shell-plugin.xml | 8 + .../src/main/resources/statechartmodel.txt | 40 ++++ .../src/test/java/demo/tasks/TasksTests.java | 179 ++++++++++++++++ .../tasks/src/test/resources/log4j.properties | 8 + 12 files changed, 602 insertions(+) create mode 100644 spring-statemachine-samples/tasks/.gitignore create mode 100644 spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java create mode 100644 spring-statemachine-samples/tasks/src/main/java/demo/tasks/StateMachineCommands.java create mode 100644 spring-statemachine-samples/tasks/src/main/java/demo/tasks/Tasks.java create mode 100644 spring-statemachine-samples/tasks/src/main/java/demo/tasks/TasksCommands.java create mode 100644 spring-statemachine-samples/tasks/src/main/resources/META-INF/spring/spring-shell-plugin.xml create mode 100644 spring-statemachine-samples/tasks/src/main/resources/statechartmodel.txt create mode 100644 spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java create mode 100644 spring-statemachine-samples/tasks/src/test/resources/log4j.properties diff --git a/build.gradle b/build.gradle index f8514999..6af1e33e 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/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/' diff --git a/settings.gradle b/settings.gradle index a614a696..706d4153 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,7 @@ include 'spring-statemachine-samples' include 'spring-statemachine-samples:turnstile' include 'spring-statemachine-samples:showcase' include 'spring-statemachine-samples:cdplayer' +include 'spring-statemachine-samples:tasks' rootProject.children.find { if (it.name == 'spring-statemachine-samples') { diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index 2b990e8d..e33c8853 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -12,3 +12,6 @@ project('spring-statemachine-samples-cdplayer') { description = 'Spring State Machine CD Player Sample' } +project('spring-statemachine-samples-tasks') { + description = 'Spring State Machine Parallel Regions Sample' +} diff --git a/spring-statemachine-samples/tasks/.gitignore b/spring-statemachine-samples/tasks/.gitignore new file mode 100644 index 00000000..70e6e4b8 --- /dev/null +++ b/spring-statemachine-samples/tasks/.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/tasks/src/main/java/demo/tasks/Application.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java new file mode 100644 index 00000000..89c6b82e --- /dev/null +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java @@ -0,0 +1,193 @@ +package demo.tasks; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +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.annotation.OnTransition; +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.READY) + .fork(States.FORK) + .state(States.TASKS) + .join(States.JOIN) + .choice(States.CHOICE) + .state(States.ERROR) + .and() + .withStates() + .parent(States.TASKS) + .initial(States.T1) + .end(States.T1E) + .and() + .withStates() + .parent(States.TASKS) + .initial(States.T2) + .end(States.T2E) + .and() + .withStates() + .parent(States.TASKS) + .initial(States.T3) + .end(States.T3E) + .and() + .withStates() + .parent(States.ERROR) + .initial(States.AUTOMATIC) + .state(States.AUTOMATIC, automaticAction(), null) + .state(States.MANUAL); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.READY).target(States.FORK) + .event(Events.RUN) + .and() + .withFork() + .source(States.FORK).target(States.TASKS) + .and() + .withExternal() + .source(States.T1).target(States.T1E) + .and() + .withExternal() + .source(States.T2).target(States.T2E) + .and() + .withExternal() + .source(States.T3).target(States.T3E) + .and() + .withJoin() + .source(States.TASKS).target(States.JOIN) + .and() + .withExternal() + .source(States.JOIN).target(States.CHOICE) + .and() + .withChoice() + .source(States.CHOICE) + .first(States.ERROR, tasksChoiceGuard()) + .last(States.READY) + .and() + .withExternal() + .source(States.ERROR).target(States.READY) + .event(Events.CONTINUE) + .and() + .withExternal() + .source(States.AUTOMATIC).target(States.MANUAL) + .event(Events.FALLBACK) + .and() + .withInternal() + .source(States.MANUAL) + .action(fixAction()) + .state(States.ERROR) + .event(Events.FIX); + } + + @Bean + public Guard tasksChoiceGuard() { + return new Guard() { + + @Override + public boolean evaluate(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + return !(variables.get("T1").equals(true) && variables.get("T2").equals(true) && variables + .get("T3").equals(true)); + } + }; + } + + @Bean + public Action automaticAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + if (variables.get("T1").equals(false)) { + variables.put("T1", true); + context.getStateMachine().sendEvent(Events.CONTINUE); + } else { + context.getStateMachine().sendEvent(Events.FALLBACK); + } + } + }; + } + + @Bean + public Action fixAction() { + return new Action() { + + @Override + public void execute(StateContext context) { + Map variables = context.getExtendedState().getVariables(); + if (variables.get("T1").equals(true) && variables.get("T2").equals(true) + && variables.get("T3").equals(true)) { + context.getStateMachine().sendEvent(Events.CONTINUE); + } + } + }; + } + + @Bean + public Tasks tasks() { + return new Tasks(); + } + + } +//end::snippetA[] + +//tag::snippetB[] + public static enum States { + READY, + FORK, JOIN, CHOICE, + TASKS, T1, T1E, T2, T2E, T3, T3E, + ERROR, AUTOMATIC, MANUAL + } +//end::snippetB[] + +//tag::snippetC[] + public static enum Events { + RUN, FALLBACK, CONTINUE, FIX; + } +//end::snippetC[] + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @OnTransition + public static @interface StatesOnTransition { + + States[] source() default {}; + + States[] target() default {}; + + } + + public static void main(String[] args) throws Exception { + Bootstrap.main(args); + } + +} diff --git a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/StateMachineCommands.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/StateMachineCommands.java new file mode 100644 index 00000000..6a287921 --- /dev/null +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/StateMachineCommands.java @@ -0,0 +1,20 @@ +package demo.tasks; + +import org.springframework.shell.core.annotation.CliCommand; +import org.springframework.shell.core.annotation.CliOption; +import org.springframework.stereotype.Component; + +import demo.AbstractStateMachineCommands; +import demo.tasks.Application.Events; +import demo.tasks.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/tasks/src/main/java/demo/tasks/Tasks.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Tasks.java new file mode 100644 index 00000000..37bdc028 --- /dev/null +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Tasks.java @@ -0,0 +1,95 @@ +package demo.tasks; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.annotation.WithStateMachine; + +import demo.tasks.Application.Events; +import demo.tasks.Application.States; +import demo.tasks.Application.StatesOnTransition; + +@WithStateMachine +public class Tasks { + + private final static Log log = LogFactory.getLog(Tasks.class); + + @Autowired + private StateMachine stateMachine; + + private final Map tasks = new HashMap(); + + public Tasks() { + tasks.put("T1", true); + tasks.put("T2", true); + tasks.put("T3", true); + } + + public void run() { + stateMachine.sendEvent(Events.RUN); + } + + public void cont() { + stateMachine.sendEvent(Events.CONTINUE); + } + + public void fix(String task) { + if (tasks.containsKey(task)) { + tasks.put(task, true); + } + stateMachine.sendEvent(Events.FIX); + } + + public void fail(String task) { + if (tasks.containsKey(task)) { + tasks.put(task, false); + } + } + + @StatesOnTransition(target = States.T1) + public void taskT1(ExtendedState extendedState) { + log.info("run task on T1"); + extendedState.getVariables().put("T1", tasks.get("T1")); + } + + @StatesOnTransition(target = States.T2) + public void taskT2(ExtendedState extendedState) { + log.info("run task on T2"); + extendedState.getVariables().put("T2", tasks.get("T2")); + } + + @StatesOnTransition(target = States.T3) + public void taskT3(ExtendedState extendedState) { + log.info("run task on T3"); + extendedState.getVariables().put("T3", tasks.get("T3")); + } + + @StatesOnTransition(target = States.AUTOMATIC) + public void automaticFix(ExtendedState extendedState) { + Map variables = extendedState.getVariables(); + if (variables.get("T1").equals(false)) { + variables.put("T1", true); + tasks.put("T1", true); + } + } + + @StatesOnTransition(target = States.MANUAL) + public void manualFix(ExtendedState extendedState) { + Map variables = extendedState.getVariables(); + if (variables.get("T2").equals(false)) { + variables.put("T2", true); + tasks.put("T2", true); + } + } + + @Override + public String toString() { + return "Tasks " + tasks; + } + +} diff --git a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/TasksCommands.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/TasksCommands.java new file mode 100644 index 00000000..cdd572fa --- /dev/null +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/TasksCommands.java @@ -0,0 +1,35 @@ +package demo.tasks; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.shell.core.CommandMarker; +import org.springframework.shell.core.annotation.CliCommand; +import org.springframework.shell.core.annotation.CliOption; +import org.springframework.stereotype.Component; + +@Component +public class TasksCommands implements CommandMarker { + + @Autowired + private Tasks tasks; + + @CliCommand(value = "tasks run", help = "Run tasks") + public void run() { + tasks.run(); + } + + @CliCommand(value = "tasks list", help = "List tasks") + public String list() { + return tasks.toString(); + } + + @CliCommand(value = "tasks fix", help = "Fix task") + public void fix(@CliOption(key = {"", "task"}, help = "Task id") String task) { + tasks.fix(task); + } + + @CliCommand(value = "tasks fail", help = "Fail task") + public void fail(@CliOption(key = {"", "task"}, help = "Task id") String task) { + tasks.fail(task); + } + +} diff --git a/spring-statemachine-samples/tasks/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/tasks/src/main/resources/META-INF/spring/spring-shell-plugin.xml new file mode 100644 index 00000000..1fc09f1f --- /dev/null +++ b/spring-statemachine-samples/tasks/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/tasks/src/main/resources/statechartmodel.txt b/spring-statemachine-samples/tasks/src/main/resources/statechartmodel.txt new file mode 100644 index 00000000..3e42dab4 --- /dev/null +++ b/spring-statemachine-samples/tasks/src/main/resources/statechartmodel.txt @@ -0,0 +1,40 @@ ++----------------------------------------------------------------------------+ +| SM | ++----------------------------------------------------------------------------+ +| | +| +---------------------------+ | +| FORK | TASKS | JOIN | +| | +---------------------------+ | | +| +-------------+ RUN | | +------------+ | | | +| *-->| READY |----->|----->| *-->| T1 |-->X |---->|----+ | +| +-------------+ | | +------------+ | | | | +| ^ ^ | |===========================| | | | +| | | | +------------+ | | | +| | | | *-->| T2 |-->X | | | +| | | | +------------+ | | | +| | | |===========================| | | +| | | | +------------+ | | | +| | | | *-->| T3 |-->X | | | +| | | | +------------+ | | | +| | | +---------------------------+ | | +| | | | | +| | | [OK] +------------+ | | +| | +--------------------------| CHOICE |<----------------+ | +| | +------------+ | +| | | | +| | | [ERROR] | +| | v | +| | +-----------------------------------------------+ | +| | | ERROR | | +| | +-----------------------------------------------+ | +| | CONTINUE | +-------------+ FALLBACK +-------------+ | | +| +-------------| *-->| AUTOMATIC |--------->| MANUAL | | | +| | | | | | | | +| | | | | FIX | | | +| | | | | +-----+ | | | +| | | | | | | | | | +| | | | | | v | | | +| | +-------------+ +-------------+ | | +| +-----------------------------------------------+ | +| | ++----------------------------------------------------------------------------+ diff --git a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java b/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java new file mode 100644 index 00000000..a0cb0b4f --- /dev/null +++ b/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java @@ -0,0 +1,179 @@ +package demo.tasks; + +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.tasks.Application.Events; +import demo.tasks.Application.States; + +public class TasksTests { + + private AnnotationConfigApplicationContext context; + + private StateMachine machine; + + private Tasks tasks; + + private TestListener listener; + + @Test + public void testInitialState() throws InterruptedException { + assertThat(listener.stateChangedLatch.await(1, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + assertThat(machine.getState().getIds(), contains(States.READY)); + } + + @Test + public void testRunOnce() throws InterruptedException { + listener.reset(3, 0, 0); + tasks.run(); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(States.READY)); + } + + @Test + public void testRunTwice() throws InterruptedException { + listener.reset(3, 0, 0); + tasks.run(); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(States.READY)); + + listener.reset(3, 0, 0); + tasks.run(); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(States.READY)); + } + + @Test + public void testFailAutomaticFix() throws InterruptedException { + listener.reset(11, 0, 0); + tasks.fail("T1"); + tasks.run(); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(11)); + assertThat(machine.getState().getIds(), contains(States.READY)); + } + + @Test + public void testFailManualFix() throws InterruptedException { + listener.reset(3, 0, 0); + tasks.fail("T2"); + tasks.run(); + tasks.fix("T2"); + tasks.cont(); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains(States.READY)); + } + + @SuppressWarnings("unchecked") + @Before + public void setup() throws Exception { + context = new AnnotationConfigApplicationContext(); + context.register(CommonConfiguration.class, Application.class, TestConfig.class); + context.refresh(); + machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + tasks = context.getBean(Tasks.class); + listener = context.getBean(TestListener.class); + machine.start(); + // lets do a little sleep to wait sm to start + Thread.sleep(1000); + } + + @After + public void clean() { + machine.stop(); + context.close(); + context = null; + machine = null; + tasks = null; + listener = null; + } + + static class TestConfig { + + @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 int stateChangedCount = 0; + volatile int transitionCount = 0; + List> statesEntered = new ArrayList>(); + List> statesExited = new ArrayList>(); + + @Override + public void stateChanged(State from, State to) { + stateChangedLatch.countDown(); + stateChangedCount++; + } + + @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 transitionEnded(Transition 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); + stateChangedCount = 0; + transitionCount = 0; + statesEntered.clear(); + statesExited.clear(); + } + + } + +} diff --git a/spring-statemachine-samples/tasks/src/test/resources/log4j.properties b/spring-statemachine-samples/tasks/src/test/resources/log4j.properties new file mode 100644 index 00000000..4589b1e9 --- /dev/null +++ b/spring-statemachine-samples/tasks/src/test/resources/log4j.properties @@ -0,0 +1,8 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} [%t] - %m%n + +log4j.category.org.springframework.statemachine=TRACE +