Add base work for recipes
- Better state sync handling #35 - Adding first recipe for synching and persisting a state # 73
This commit is contained in:
22
build.gradle
22
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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<S, E> {
|
||||
*/
|
||||
void setExtendedState(ExtendedState extendedState);
|
||||
|
||||
/**
|
||||
* Adds the state change interceptor.
|
||||
*
|
||||
* @param interceptor the interceptor
|
||||
*/
|
||||
void addStateChangeInterceptor(StateChangeInterceptor<S, E> interceptor);
|
||||
|
||||
}
|
||||
|
||||
@@ -259,6 +259,8 @@ public abstract class AbstractStateMachine<S, E> 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<S, E> extends StateMachineObjectSuppo
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStateChangeInterceptor(StateChangeInterceptor<S, E> interceptor) {
|
||||
getStateChangeInterceptors().add(interceptor);
|
||||
}
|
||||
|
||||
protected boolean acceptEvent(Message<E> message) {
|
||||
|
||||
boolean accepted = currentState.sendEvent(message);
|
||||
@@ -430,7 +437,20 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean callStateChangeInterceptors(State<S,E> state, Message<E> message, Transition<S,E> transition, StateMachine<S, E> 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<S,E> state, Message<E> message, Transition<S,E> transition, StateMachine<S, E> 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;
|
||||
|
||||
@@ -147,6 +147,13 @@ public class DefaultStateMachineExecutor<S, E> 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<Transition<S, E>> trans, Message<E> queuedMessage) {
|
||||
for (Transition<S, E> t : trans) {
|
||||
if (t == null) {
|
||||
|
||||
@@ -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<S, E> {
|
||||
|
||||
void preStateChange(State<S,E> state, Message<E> message, Transition<S,E> transition, StateMachine<S, E> stateMachine);
|
||||
|
||||
}
|
||||
@@ -58,6 +58,13 @@ public interface StateMachineExecutor<S, E> {
|
||||
*/
|
||||
void execute();
|
||||
|
||||
/**
|
||||
* Sets the if initial stage is enabled.
|
||||
*
|
||||
* @param enabled the new flag
|
||||
*/
|
||||
void setInitialEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Start executor.
|
||||
*
|
||||
|
||||
@@ -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<S, E> 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<S, E> extends LifecycleObjectSup
|
||||
// re-scheduling is needed.
|
||||
}
|
||||
|
||||
protected StateChangeInterceptorList getStateChangeInterceptors() {
|
||||
return interceptors;
|
||||
}
|
||||
|
||||
protected void setStateChangeInterceptors(List<StateChangeInterceptor<S,E>> 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<S, E> extends LifecycleObjectSup
|
||||
|
||||
}
|
||||
|
||||
protected class StateChangeInterceptorList {
|
||||
|
||||
private final List<StateChangeInterceptor<S, E>> interceptors = new CopyOnWriteArrayList<StateChangeInterceptor<S, E>>();
|
||||
|
||||
/**
|
||||
* Sets the interceptors, clears any existing interceptors.
|
||||
*
|
||||
* @param interceptors the list of interceptors
|
||||
* @return <tt>true</tt> if interceptor list changed as a result of the
|
||||
* call
|
||||
*/
|
||||
public boolean set(List<StateChangeInterceptor<S, E>> interceptors) {
|
||||
synchronized (interceptors) {
|
||||
interceptors.clear();
|
||||
return interceptors.addAll(interceptors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds interceptor to the list.
|
||||
*
|
||||
* @param interceptor the interceptor
|
||||
* @return <tt>true</tt> (as specified by {@link Collection#add})
|
||||
*/
|
||||
public boolean add(StateChangeInterceptor<S, E> interceptor) {
|
||||
return interceptors.add(interceptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes interceptor from the list.
|
||||
*
|
||||
* @param interceptor the interceptor
|
||||
* @return <tt>true</tt> (as specified by {@link Collection#remove})
|
||||
*/
|
||||
public boolean remove(StateChangeInterceptor<S, E> interceptor) {
|
||||
return interceptors.remove(interceptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the pre state change calls.
|
||||
*/
|
||||
void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
|
||||
StateMachine<S, E> stateMachine) {
|
||||
for (StateChangeInterceptor<S, E> interceptor : interceptors) {
|
||||
interceptor.preStateChange(state, message, transition, stateMachine);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, String> interceptor) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRelay(StateMachine<String, String> stateMachine) {
|
||||
this.relay = stateMachine;
|
||||
|
||||
2
spring-statemachine-recipes/build.gradle
Normal file
2
spring-statemachine-recipes/build.gradle
Normal file
@@ -0,0 +1,2 @@
|
||||
description = 'Spring State Machine Recipes Common'
|
||||
|
||||
@@ -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<String, String> 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<String, String> stateMachine) {
|
||||
Assert.notNull(stateMachine, "State machine must be set");
|
||||
this.stateMachine = stateMachine;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInit() throws Exception {
|
||||
stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<String,String>>() {
|
||||
|
||||
@Override
|
||||
public void apply(StateMachineAccess<String, String> function) {
|
||||
function.addStateChangeInterceptor(interceptor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event with entity.
|
||||
*
|
||||
* @param event the event
|
||||
* @param state the state
|
||||
*/
|
||||
public void handleEventWithState(Message<String> event, String state) {
|
||||
stateMachine.stop();
|
||||
List<StateMachineAccess<String, String>> withAllRegions = stateMachine.getStateMachineAccessor().withAllRegions();
|
||||
for (StateMachineAccess<String, String> 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 <code>addPersistStateChangeListener<code> 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<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine);
|
||||
}
|
||||
|
||||
private class PersistingStateChangeInterceptor implements StateChangeInterceptor<String, String> {
|
||||
|
||||
@Override
|
||||
public void preStateChange(State<String, String> state, Message<String> message,
|
||||
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
|
||||
listeners.onPersist(state, message, transition, stateMachine);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class CompositePersistStateChangeListener extends AbstractCompositeListener<PersistStateChangeListener> implements
|
||||
PersistStateChangeListener {
|
||||
|
||||
@Override
|
||||
public void onPersist(State<String, String> state, Message<String> message,
|
||||
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
|
||||
for (Iterator<PersistStateChangeListener> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
PersistStateChangeListener listener = iterator.next();
|
||||
listener.onPersist(state, message, transition, stateMachine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
spring-statemachine-samples/persist/.gitignore
vendored
Normal file
19
spring-statemachine-samples/persist/.gitignore
vendored
Normal file
@@ -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
|
||||
@@ -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<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states)
|
||||
throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("PLACED")
|
||||
.state("PROCESSING")
|
||||
.state("SENT")
|
||||
.state("DELIVERED");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<String, String> 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<String, String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Order> orders = jdbcTemplate.query(
|
||||
"select id, state from orders",
|
||||
new RowMapper<Order>() {
|
||||
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<Order>() {
|
||||
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<String, String> state, Message<String> message,
|
||||
Transition<String, String> transition, StateMachine<String, String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, String> {
|
||||
|
||||
@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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
|
||||
<context:component-scan base-package="demo" />
|
||||
</beans>
|
||||
@@ -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');
|
||||
@@ -0,0 +1,4 @@
|
||||
create table orders (
|
||||
id int,
|
||||
state varchar(256)
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
+---------------------------------------------------------------+
|
||||
| SM |
|
||||
+---------------------------------------------------------------+
|
||||
| |
|
||||
| +----------------+ +----------------+ |
|
||||
| *-->| PLACED | | PROCESSING | |
|
||||
| +----------------+ PROCESS +----------------+ SEND |
|
||||
| | |---------->| |-----+ |
|
||||
| +----------------+ +----------------+ | |
|
||||
| | |
|
||||
| | |
|
||||
| +----------------+ +----------------+ | |
|
||||
| | SENT | | DELIVERED | | |
|
||||
| +----------------+ DELIVER +----------------+ | |
|
||||
| | |<----------| |<----+ |
|
||||
| +----------------+ +----------------+ |
|
||||
| |
|
||||
+---------------------------------------------------------------+
|
||||
@@ -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<String, String> 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<String, String> {
|
||||
|
||||
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<Transition<String, String>> transitions = new ArrayList<Transition<String, String>>();
|
||||
List<State<String, String>> statesEntered = new ArrayList<State<String, String>>();
|
||||
List<State<String, String>> statesExited = new ArrayList<State<String, String>>();
|
||||
volatile int transitionCount = 0;
|
||||
|
||||
@Override
|
||||
public void stateChanged(State<String, String> from, State<String, String> to) {
|
||||
stateChangedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateEntered(State<String, String> state) {
|
||||
statesEntered.add(state);
|
||||
stateEnteredLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateExited(State<String, String> state) {
|
||||
statesExited.add(state);
|
||||
stateExitedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transition(Transition<String, String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user