From 1bcdbd5ae048da4952ca65a08ffd42a2e8ee44f4 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Wed, 17 Jun 2015 09:19:43 +0100 Subject: [PATCH] Add base work for recipes - Better state sync handling #35 - Adding first recipe for synching and persisting a state # 73 --- build.gradle | 22 +++ settings.gradle | 6 + .../access/StateMachineAccess.java | 8 + .../support/AbstractStateMachine.java | 20 +++ .../support/DefaultStateMachineExecutor.java | 7 + .../support/StateChangeInterceptor.java | 27 ++++ .../support/StateMachineExecutor.java | 7 + .../support/StateMachineObjectSupport.java | 68 +++++++++ .../access/StateMachineAccessTests.java | 5 + spring-statemachine-recipes/build.gradle | 2 + .../persist/PersistStateMachineHandler.java | 136 +++++++++++++++++ spring-statemachine-samples/build.gradle | 10 ++ .../persist/.gitignore | 19 +++ .../main/java/demo/persist/Application.java | 108 +++++++++++++ .../src/main/java/demo/persist/Persist.java | 86 +++++++++++ .../java/demo/persist/PersistCommands.java | 50 ++++++ .../demo/persist/StateMachineCommands.java | 33 ++++ .../META-INF/spring/spring-shell-plugin.xml | 8 + .../persist/src/main/resources/data.sql | 4 + .../persist/src/main/resources/schema.sql | 4 + .../src/main/resources/statechartmodel.txt | 18 +++ .../test/java/demo/persist/PersistTests.java | 144 ++++++++++++++++++ 22 files changed, 792 insertions(+) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateChangeInterceptor.java create mode 100644 spring-statemachine-recipes/build.gradle create mode 100644 spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/persist/PersistStateMachineHandler.java create mode 100644 spring-statemachine-samples/persist/.gitignore create mode 100644 spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java create mode 100644 spring-statemachine-samples/persist/src/main/java/demo/persist/Persist.java create mode 100644 spring-statemachine-samples/persist/src/main/java/demo/persist/PersistCommands.java create mode 100644 spring-statemachine-samples/persist/src/main/java/demo/persist/StateMachineCommands.java create mode 100644 spring-statemachine-samples/persist/src/main/resources/META-INF/spring/spring-shell-plugin.xml create mode 100644 spring-statemachine-samples/persist/src/main/resources/data.sql create mode 100644 spring-statemachine-samples/persist/src/main/resources/schema.sql create mode 100644 spring-statemachine-samples/persist/src/main/resources/statechartmodel.txt create mode 100644 spring-statemachine-samples/persist/src/test/java/demo/persist/PersistTests.java diff --git a/build.gradle b/build.gradle index 32c59313..6c2eb380 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,12 @@ buildscript { } } +def recipeProjects() { + subprojects.findAll { project -> + project.name.contains('spring-statemachine-recipes') && project.name != 'spring-statemachine-recipes-common' + } +} + def sampleProjects() { subprojects.findAll { project -> project.name.contains('spring-statemachine-samples') && project.name != 'spring-statemachine-samples-common' @@ -136,6 +142,22 @@ project('spring-statemachine-zookeeper') { } } +configure(recipeProjects()) { + dependencies { + compile project(":spring-statemachine-recipes-common") + testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" + testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" + testCompile "junit:junit:$junitVersion" + } +} + +project('spring-statemachine-recipes-common') { + dependencies { + compile project(":spring-statemachine-core") + } +} + configure(sampleProjects()) { apply plugin: 'spring-boot' configurations.archives.artifacts.removeAll { it.archiveTask.is jar } diff --git a/settings.gradle b/settings.gradle index ed0bd9ec..4ee8d2ac 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,8 @@ rootProject.name = 'spring-statemachine' include 'spring-statemachine-core' include 'spring-statemachine-zookeeper' +include 'spring-statemachine-recipes' + include 'spring-statemachine-samples' include 'spring-statemachine-samples:turnstile' include 'spring-statemachine-samples:showcase' @@ -10,8 +12,12 @@ include 'spring-statemachine-samples:cdplayer' include 'spring-statemachine-samples:tasks' include 'spring-statemachine-samples:washer' include 'spring-statemachine-samples:zookeeper' +include 'spring-statemachine-samples:persist' rootProject.children.find { + if (it.name == 'spring-statemachine-recipes') { + it.name = 'spring-statemachine-recipes-common' + } if (it.name == 'spring-statemachine-samples') { it.name = 'spring-statemachine-samples-common' it.children.each { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java index f84c6d35..7528049d 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/access/StateMachineAccess.java @@ -17,6 +17,7 @@ package org.springframework.statemachine.access; import org.springframework.statemachine.ExtendedState; import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.support.StateChangeInterceptor; /** * Functional interface exposing {@link StateMachine} internals. @@ -49,4 +50,11 @@ public interface StateMachineAccess { */ void setExtendedState(ExtendedState extendedState); + /** + * Adds the state change interceptor. + * + * @param interceptor the interceptor + */ + void addStateChangeInterceptor(StateChangeInterceptor interceptor); + } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index cc9222b0..de1e6c9e 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -259,6 +259,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo protected void doStart() { // if state is set assume nothing to do if (currentState != null) { + stateMachineExecutor.setInitialEnabled(false); + stateMachineExecutor.start(); return; } registerPseudoStateListener(); @@ -396,6 +398,11 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } } + @Override + public void addStateChangeInterceptor(StateChangeInterceptor interceptor) { + getStateChangeInterceptors().add(interceptor); + } + protected boolean acceptEvent(Message message) { boolean accepted = currentState.sendEvent(message); @@ -430,7 +437,20 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo return false; } + private boolean callStateChangeInterceptors(State state, Message message, Transition transition, StateMachine stateMachine) { + try { + getStateChangeInterceptors().preStateChange(state, message, transition, stateMachine); + } catch (Exception e) { + log.info("Interceptors threw and exception, skipping state change", e); + return false; + } + return true; + } + private void switchToState(State state, Message message, Transition transition, StateMachine stateMachine) { + if (!callStateChangeInterceptors(state, message, transition, stateMachine)) { + return; + } // TODO: need to make below more clear when // we figure out rest of a pseudostates PseudoStateKind kind = state.getPseudoState() != null ? state.getPseudoState().getKind() : null; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java index 12f7a8b9..18ce5f3a 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java @@ -147,6 +147,13 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im initialHandled.set(false); } + @Override + public void setInitialEnabled(boolean enabled) { + // TODO: should prob handle case where this is enabled + // when executor is running + initialHandled.set(!enabled); + } + private void handleTriggerTrans(List> trans, Message queuedMessage) { for (Transition t : trans) { if (t == null) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateChangeInterceptor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateChangeInterceptor.java new file mode 100644 index 00000000..476407b7 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateChangeInterceptor.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.support; + +import org.springframework.messaging.Message; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; + +public interface StateChangeInterceptor { + + void preStateChange(State state, Message message, Transition transition, StateMachine stateMachine); + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java index 974e495c..25f7214d 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java @@ -58,6 +58,13 @@ public interface StateMachineExecutor { */ void execute(); + /** + * Sets the if initial stage is enabled. + * + * @param enabled the new flag + */ + void setInitialEnabled(boolean enabled); + /** * Start executor. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java index 6a3cd680..7f14a42b 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java @@ -15,8 +15,15 @@ */ package org.springframework.statemachine.support; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.OrderComparator; +import org.springframework.messaging.Message; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.event.StateMachineEventPublisher; @@ -46,6 +53,9 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup /** Flag for application context events */ private boolean contextEventsEnabled = true; + private final StateChangeInterceptorList interceptors = + new StateChangeInterceptorList(); + /** * Gets the state machine event publisher. * @@ -186,6 +196,15 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup // re-scheduling is needed. } + protected StateChangeInterceptorList getStateChangeInterceptors() { + return interceptors; + } + + protected void setStateChangeInterceptors(List> interceptors) { + Collections.sort(interceptors, new OrderComparator()); + this.interceptors.set(interceptors); + } + /** * This class is used to relay listener events from a submachines which works * as its own listener context. User only connects to main root machine and @@ -241,4 +260,53 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } + protected class StateChangeInterceptorList { + + private final List> interceptors = new CopyOnWriteArrayList>(); + + /** + * Sets the interceptors, clears any existing interceptors. + * + * @param interceptors the list of interceptors + * @return true if interceptor list changed as a result of the + * call + */ + public boolean set(List> interceptors) { + synchronized (interceptors) { + interceptors.clear(); + return interceptors.addAll(interceptors); + } + } + + /** + * Adds interceptor to the list. + * + * @param interceptor the interceptor + * @return true (as specified by {@link Collection#add}) + */ + public boolean add(StateChangeInterceptor interceptor) { + return interceptors.add(interceptor); + } + + /** + * Removes interceptor from the list. + * + * @param interceptor the interceptor + * @return true (as specified by {@link Collection#remove}) + */ + public boolean remove(StateChangeInterceptor interceptor) { + return interceptors.remove(interceptor); + } + + /** + * Handles the pre state change calls. + */ + void preStateChange(State state, Message message, Transition transition, + StateMachine stateMachine) { + for (StateChangeInterceptor interceptor : interceptors) { + interceptor.preStateChange(state, message, transition, stateMachine); + } + } + + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java index 591e352b..db6d7300 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/access/StateMachineAccessTests.java @@ -28,6 +28,7 @@ import org.springframework.statemachine.ExtendedState; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.state.State; +import org.springframework.statemachine.support.StateChangeInterceptor; import org.springframework.statemachine.transition.Transition; public class StateMachineAccessTests { @@ -80,6 +81,10 @@ public class StateMachineAccessTests { }; } + @Override + public void addStateChangeInterceptor(StateChangeInterceptor interceptor) { + } + @Override public void setRelay(StateMachine stateMachine) { this.relay = stateMachine; diff --git a/spring-statemachine-recipes/build.gradle b/spring-statemachine-recipes/build.gradle new file mode 100644 index 00000000..76e7c766 --- /dev/null +++ b/spring-statemachine-recipes/build.gradle @@ -0,0 +1,2 @@ +description = 'Spring State Machine Recipes Common' + diff --git a/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/persist/PersistStateMachineHandler.java b/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/persist/PersistStateMachineHandler.java new file mode 100644 index 00000000..a270a51a --- /dev/null +++ b/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/persist/PersistStateMachineHandler.java @@ -0,0 +1,136 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.recipes.persist; + +import java.util.Iterator; +import java.util.List; + +import org.springframework.messaging.Message; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.access.StateMachineAccess; +import org.springframework.statemachine.access.StateMachineFunction; +import org.springframework.statemachine.listener.AbstractCompositeListener; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.support.LifecycleObjectSupport; +import org.springframework.statemachine.support.StateChangeInterceptor; +import org.springframework.statemachine.transition.Transition; +import org.springframework.util.Assert; + +/** + * {@code PersistStateMachineHandler} is a recipe which can be used to + * handle a state change of an arbitrary entity in a persistent storage. + * + * @author Janne Valkealahti + * + */ +public class PersistStateMachineHandler extends LifecycleObjectSupport { + + private final StateMachine stateMachine; + private final PersistingStateChangeInterceptor interceptor = new PersistingStateChangeInterceptor(); + private final CompositePersistStateChangeListener listeners = new CompositePersistStateChangeListener(); + + /** + * Instantiates a new persist state machine handler. + * + * @param stateMachine the state machine + */ + public PersistStateMachineHandler(StateMachine stateMachine) { + Assert.notNull(stateMachine, "State machine must be set"); + this.stateMachine = stateMachine; + } + + @Override + protected void onInit() throws Exception { + stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess function) { + function.addStateChangeInterceptor(interceptor); + } + }); + } + + /** + * Handle event with entity. + * + * @param event the event + * @param state the state + */ + public void handleEventWithState(Message event, String state) { + stateMachine.stop(); + List> withAllRegions = stateMachine.getStateMachineAccessor().withAllRegions(); + for (StateMachineAccess a : withAllRegions) { + a.resetState(state); + } + stateMachine.start(); + stateMachine.sendEvent(event); + } + + /** + * Adds the persist state change listener. + * + * @param listener the listener + */ + public void addPersistStateChangeListener(PersistStateChangeListener listener) { + listeners.register(listener); + } + + /** + * The listener interface for receiving persistStateChange events. + * The class that is interested in processing a persistStateChange + * event implements this interface, and the object created + * with that class is registered with a component using the + * component's addPersistStateChangeListener method. When + * the persistStateChange event occurs, that object's appropriate + * method is invoked. + */ + public interface PersistStateChangeListener { + + /** + * Called when state needs to be persisted. + * + * @param state the state + * @param message the message + * @param transition the transition + * @param stateMachine the state machine + */ + void onPersist(State state, Message message, Transition transition, StateMachine stateMachine); + } + + private class PersistingStateChangeInterceptor implements StateChangeInterceptor { + + @Override + public void preStateChange(State state, Message message, + Transition transition, StateMachine stateMachine) { + listeners.onPersist(state, message, transition, stateMachine); + } + + } + + private class CompositePersistStateChangeListener extends AbstractCompositeListener implements + PersistStateChangeListener { + + @Override + public void onPersist(State state, Message message, + Transition transition, StateMachine stateMachine) { + for (Iterator iterator = getListeners().reverse(); iterator.hasNext();) { + PersistStateChangeListener listener = iterator.next(); + listener.onPersist(state, message, transition, stateMachine); + } + } + } + +} diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index 14b92a4d..d4224ac2 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -23,3 +23,13 @@ project('spring-statemachine-samples-washer') { project('spring-statemachine-samples-zookeeper') { description = 'Spring State Machine Distributed Sample' } + +project('spring-statemachine-samples-persist') { + description = 'Spring State Machine Persist Sample' + dependencies { + compile project(":spring-statemachine-recipes-common") + compile ("org.hsqldb:hsqldb:2.3.1") + compile ("org.springframework:spring-jdbc:$springVersion") + } +} + diff --git a/spring-statemachine-samples/persist/.gitignore b/spring-statemachine-samples/persist/.gitignore new file mode 100644 index 00000000..70e6e4b8 --- /dev/null +++ b/spring-statemachine-samples/persist/.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/persist/src/main/java/demo/persist/Application.java b/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java new file mode 100644 index 00000000..862b9d56 --- /dev/null +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/Application.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package demo.persist; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.shell.Bootstrap; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler; + +@SpringBootApplication +public class Application { + +//tag::snippetA[] + @Configuration + @EnableStateMachine + static class StateMachineConfig + extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("PLACED") + .state("PROCESSING") + .state("SENT") + .state("DELIVERED"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("PLACED").target("PROCESSING") + .event("PROCESS") + .and() + .withExternal() + .source("PROCESSING").target("SENT") + .event("SEND") + .and() + .withExternal() + .source("SENT").target("DELIVERED") + .event("DELIVER"); + } + + } +//end::snippetA[] + + @Configuration + static class PersistHandlerConfig { + + @Autowired + private StateMachine stateMachine; + + @Bean + public Persist persist() { + return new Persist(persistStateMachineHandler()); + } + + @Bean + public PersistStateMachineHandler persistStateMachineHandler() { + return new PersistStateMachineHandler(stateMachine); + } + + } + + public static class Order { + int id; + String state; + + public Order(int id, String state) { + this.id = id; + this.state = state; + } + + @Override + public String toString() { + return "Order [id=" + id + ", state=" + state + "]"; + } + + } + + 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 new file mode 100644 index 00000000..dca88c3f --- /dev/null +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/Persist.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package demo.persist; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler; +import org.springframework.statemachine.recipes.persist.PersistStateMachineHandler.PersistStateChangeListener; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; + +import demo.persist.Application.Order; + +public class Persist { + + private final PersistStateMachineHandler handler; + + @Autowired + private JdbcTemplate jdbcTemplate; + + private final PersistStateChangeListener listener = new LocalPersistStateChangeListener(); + + public Persist(PersistStateMachineHandler handler) { + this.handler = handler; + this.handler.addPersistStateChangeListener(listener); + } + + public String listDbEntries() { + List orders = jdbcTemplate.query( + "select id, state from orders", + new RowMapper() { + public Order mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Order(rs.getInt("id"), rs.getString("state")); + } + }); + StringBuilder buf = new StringBuilder(); + for (Order order : orders) { + buf.append(order); + buf.append("\n"); + } + return buf.toString(); + } + + 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 { + return new Order(rs.getInt("id"), rs.getString("state")); + } + }); + handler.handleEventWithState(MessageBuilder.withPayload(event).setHeader("order", order).build(), o.state); + } + + private class LocalPersistStateChangeListener implements PersistStateChangeListener { + + @Override + public void onPersist(State state, Message message, + Transition transition, StateMachine stateMachine) { + if (message != null && message.getHeaders().containsKey("order")) { + Integer order = message.getHeaders().get("order", Integer.class); + jdbcTemplate.update("update orders set state = ? where id = ?", state.getId(), order); + } + } + } + +} diff --git a/spring-statemachine-samples/persist/src/main/java/demo/persist/PersistCommands.java b/spring-statemachine-samples/persist/src/main/java/demo/persist/PersistCommands.java new file mode 100644 index 00000000..927a50f5 --- /dev/null +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/PersistCommands.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package demo.persist; + +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 PersistCommands implements CommandMarker { + + @Autowired + private Persist persist; + + @CliCommand(value = "persist db", help = "List entries from db") + public String listDbEntries() { + return persist.listDbEntries(); + } + + @CliCommand(value = "persist process", help = "Process order") + public void process(@CliOption(key = {"", "id"}, help = "Order id") int order) { + persist.change(order, "PROCESS"); + } + + @CliCommand(value = "persist send", help = "Send order") + public void send(@CliOption(key = {"", "id"}, help = "Order id") int order) { + persist.change(order, "SEND"); + } + + @CliCommand(value = "persist deliver", help = "Deliver order") + public void deliver(@CliOption(key = {"", "id"}, help = "Order id") int order) { + persist.change(order, "DELIVER"); + } + +} diff --git a/spring-statemachine-samples/persist/src/main/java/demo/persist/StateMachineCommands.java b/spring-statemachine-samples/persist/src/main/java/demo/persist/StateMachineCommands.java new file mode 100644 index 00000000..033eb60f --- /dev/null +++ b/spring-statemachine-samples/persist/src/main/java/demo/persist/StateMachineCommands.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package demo.persist; + +import org.springframework.shell.core.annotation.CliCommand; +import org.springframework.shell.core.annotation.CliOption; +import org.springframework.stereotype.Component; + +import demo.AbstractStateMachineCommands; + +@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 String event) { + getStateMachine().sendEvent(event); + return "Event " + event + " send"; + } + +} \ No newline at end of file diff --git a/spring-statemachine-samples/persist/src/main/resources/META-INF/spring/spring-shell-plugin.xml b/spring-statemachine-samples/persist/src/main/resources/META-INF/spring/spring-shell-plugin.xml new file mode 100644 index 00000000..1fc09f1f --- /dev/null +++ b/spring-statemachine-samples/persist/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/persist/src/main/resources/data.sql b/spring-statemachine-samples/persist/src/main/resources/data.sql new file mode 100644 index 00000000..48c89228 --- /dev/null +++ b/spring-statemachine-samples/persist/src/main/resources/data.sql @@ -0,0 +1,4 @@ +insert into orders (id, state) values (1, 'PLACED'); +insert into orders (id, state) values (2, 'PROCESSING'); +insert into orders (id, state) values (3, 'SENT'); +insert into orders (id, state) values (4, 'DELIVERED'); diff --git a/spring-statemachine-samples/persist/src/main/resources/schema.sql b/spring-statemachine-samples/persist/src/main/resources/schema.sql new file mode 100644 index 00000000..aeed9934 --- /dev/null +++ b/spring-statemachine-samples/persist/src/main/resources/schema.sql @@ -0,0 +1,4 @@ +create table orders ( + id int, + state varchar(256) +); diff --git a/spring-statemachine-samples/persist/src/main/resources/statechartmodel.txt b/spring-statemachine-samples/persist/src/main/resources/statechartmodel.txt new file mode 100644 index 00000000..183d9eba --- /dev/null +++ b/spring-statemachine-samples/persist/src/main/resources/statechartmodel.txt @@ -0,0 +1,18 @@ ++---------------------------------------------------------------+ +| SM | ++---------------------------------------------------------------+ +| | +| +----------------+ +----------------+ | +| *-->| PLACED | | PROCESSING | | +| +----------------+ PROCESS +----------------+ SEND | +| | |---------->| |-----+ | +| +----------------+ +----------------+ | | +| | | +| | | +| +----------------+ +----------------+ | | +| | SENT | | DELIVERED | | | +| +----------------+ DELIVER +----------------+ | | +| | |<----------| |<----+ | +| +----------------+ +----------------+ | +| | ++---------------------------------------------------------------+ diff --git a/spring-statemachine-samples/persist/src/test/java/demo/persist/PersistTests.java b/spring-statemachine-samples/persist/src/test/java/demo/persist/PersistTests.java new file mode 100644 index 00000000..fda8c2db --- /dev/null +++ b/spring-statemachine-samples/persist/src/test/java/demo/persist/PersistTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package demo.persist; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +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.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import demo.CommonConfiguration; + +@RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD) +@SpringApplicationConfiguration(classes = { CommonConfiguration.class, Application.class, StateMachineCommands.class }) +public class PersistTests { + + @Autowired + private StateMachineCommands commands; + + @Autowired + private StateMachine machine; + + @Autowired + private Persist persist; + + @Test + public void testNotStarted() throws Exception { + assertThat(commands.state(), is("No state")); + } + + @Test + public void testInitialState() throws Exception { + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateChangedLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), contains("PLACED")); + assertThat(listener.statesEntered.size(), is(1)); + assertThat(listener.statesEntered.get(0).getId(), is("PLACED")); + assertThat(listener.statesExited.size(), is(0)); + } + + @Test + public void testInitialDbList() { + // dataOrder [id=1, state=PLACED]Order [id=2, state=PROCESSING]Order [id=3, state=SENT]Order [id=4, state=DELIVERED] + assertThat(persist.listDbEntries(), containsString("PLACED")); + } + + @Test + public void testUpdate1() { + persist.change(1, "PROCESS"); + assertThat(persist.listDbEntries(), containsString("id=1, state=PROCESSING")); + } + + @Test + public void testUpdate2() { + persist.change(2, "SEND"); + assertThat(persist.listDbEntries(), containsString("id=2, state=SENT")); + } + + private static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile CountDownLatch stateEnteredLatch = new CountDownLatch(1); + 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); + transitionCount++; + transitionLatch.countDown(); + } + + 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(); + } + + } + +}