Refactor region handling and persistence

- Make kryo in AbstractKryoStateMachineSerialisationService aware of same classloader
  most likely use in an app. This takes away some of those weird kryo
  errors you see with a web apps.
- Add context references concept to StateMachineContext which can be used
  to store reference id and then individual running machines with regions
  can independently store their states. Whole machine state can then get
  restored more accurately.
- Add new `region(String id)` to StateConfigurer which can be used to set region id.
  This is equivalent as setting region id with json based machine structure where
  you need to define region id's with orthogonal regions are in use.
- Add new datajpamultipersist sample showing running regions and how those are
  persisted to a database.
- Fixes #617
- Fixes #605
- Fixes #615
This commit is contained in:
Janne Valkealahti
2019-01-12 08:36:51 +00:00
parent c700301767
commit 84ca0aec3e
34 changed files with 1297 additions and 51 deletions

View File

@@ -27,6 +27,7 @@ include 'spring-statemachine-samples:eventservice'
include 'spring-statemachine-samples:deploy'
include 'spring-statemachine-samples:ordershipping'
include 'spring-statemachine-samples:datajpa'
include 'spring-statemachine-samples:datajpamultipersist'
include 'spring-statemachine-samples:datapersist'
include 'spring-statemachine-samples:monitoring'

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-2019 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.
@@ -42,6 +42,13 @@ public interface StateMachineContext<S, E> {
*/
List<StateMachineContext<S, E>> getChilds();
/**
* Gets the child context references if any.
*
* @return the child context references
*/
List<String> getChildReferences();
/**
* Gets the state.
*

View File

@@ -80,6 +80,7 @@ import org.springframework.statemachine.state.StateMachineState;
import org.springframework.statemachine.support.DefaultExtendedState;
import org.springframework.statemachine.support.LifecycleObjectSupport;
import org.springframework.statemachine.support.StateMachineInterceptor;
import org.springframework.statemachine.support.StateMachineInterceptorAdapter;
import org.springframework.statemachine.support.tree.Tree;
import org.springframework.statemachine.support.tree.Tree.Node;
import org.springframework.statemachine.support.tree.TreeTraverser;
@@ -219,9 +220,14 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
if (initialCount > 1) {
for (Collection<StateData<S, E>> regionStateDatas : regionsStateDatas) {
// try to build reqion id's
Object rId = regionStateDatas.iterator().next().getRegion();
String mId = machineId != null ? machineId : stateMachineModel.getConfigurationData().getMachineId();
mId = mId + "#" + (rId != null ? rId.toString() : "");
machine = buildMachine(machineMap, stateMap, holderList, regionStateDatas, transitionsData, resolveBeanFactory(stateMachineModel),
contextEvents, defaultExtendedState, stateMachineModel.getTransitionsData(), resolveTaskExecutor(stateMachineModel),
resolveTaskScheduler(stateMachineModel), machineId, null, stateMachineModel);
resolveTaskScheduler(stateMachineModel), mId, null, stateMachineModel);
regionStack.push(new MachineStackItem<S, E>(machine));
machines.add(machine);
}
@@ -359,10 +365,12 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
List<StateMachineInterceptor<S,E>> interceptors = stateMachineModel.getConfigurationData().getStateMachineInterceptors();
if (interceptors != null) {
for (final StateMachineInterceptor<S, E> interceptor : interceptors) {
machine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S,E>>() {
// add persisting interceptor hooks to all regions
RegionPersistingInterceptorAdapter<S, E> adapter = new RegionPersistingInterceptorAdapter<>(interceptor, machine);
machine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<S,E>>() {
@Override
public void apply(StateMachineAccess<S, E> function) {
function.addStateMachineInterceptor(interceptor);
function.addStateMachineInterceptor(adapter);
}
});
}
@@ -377,6 +385,42 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
return delegateAutoStartup(machine);
}
private static class RegionPersistingInterceptorAdapter<S, E> extends StateMachineInterceptorAdapter<S, E> {
private final StateMachineInterceptor<S, E> interceptor;
private final StateMachine<S, E> rootStateMachine;
public RegionPersistingInterceptorAdapter(StateMachineInterceptor<S, E> interceptor, StateMachine<S, E> rootStateMachine) {
this.interceptor = interceptor;
this.rootStateMachine = rootStateMachine;
}
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {
interceptor.preStateChange(state, message, transition, stateMachine, rootStateMachine);
}
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
interceptor.preStateChange(state, message, transition, stateMachine, rootStateMachine);
}
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {
interceptor.postStateChange(state, message, transition, stateMachine, rootStateMachine);
}
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
interceptor.postStateChange(state, message, transition, stateMachine, rootStateMachine);
}
}
/**
* Instructs this factory to handle auto-start flag manually
* by calling lifecycle start method.

View File

@@ -47,7 +47,7 @@ public class DefaultStateConfigurer<S, E>
implements StateConfigurer<S, E> {
private Object parent;
private final Object region = UUID.randomUUID().toString();
private Object region = UUID.randomUUID().toString();
private final Map<S, StateData<S, E>> incomplete = new HashMap<S, StateData<S, E>>();
private S initialState;
private Action<S, E> initialAction;
@@ -123,6 +123,12 @@ public class DefaultStateConfigurer<S, E>
return this;
}
@Override
public StateConfigurer<S, E> region(String id) {
this.region = id;
return this;
}
@Override
public StateConfigurer<S, E> end(S end) {
this.ends.add(end);

View File

@@ -63,6 +63,14 @@ public interface StateConfigurer<S, E> extends
*/
StateConfigurer<S, E> parent(S state);
/**
* Specify a region for these states configured by this configurer instance.
*
* @param id the region id
* @return configurer for chaining
*/
StateConfigurer<S, E> region(String id);
/**
* Specify a state {@code S}.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-2019 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.
@@ -211,11 +211,22 @@ public class DistributedStateMachine<S, E> extends LifecycleObjectSupport implem
}
}
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
preStateChange(state, message, transition, stateMachine);
}
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {
}
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
}
@Override
public StateContext<S, E> preTransition(StateContext<S, E> stateContext) {
return stateContext;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 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.
@@ -18,8 +18,11 @@ package org.springframework.statemachine.persist;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.messaging.Message;
import org.springframework.statemachine.ExtendedState;
import org.springframework.statemachine.StateMachine;
@@ -56,36 +59,61 @@ import org.springframework.util.Assert;
public abstract class AbstractPersistingStateMachineInterceptor<S, E, T> extends StateMachineInterceptorAdapter<S, E>
implements StateMachinePersist<S, E, T> {
private static final Log log = LogFactory.getLog(AbstractPersistingStateMachineInterceptor.class);
private Function<StateMachine<S, E>, Map<Object, Object>> extendedStateVariablesFunction = new AllVariablesFunction<>();
@SuppressWarnings("unchecked")
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition, StateMachine<S, E> stateMachine) {
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
if (log.isDebugEnabled()) {
log.debug("preStateChange with stateMachine " + stateMachine);
log.debug("preStateChange with root stateMachine " + rootStateMachine);
log.debug("preStateChange with state " + state);
}
// try to persist context and in case of failure, interceptor
// call chain aborts transition
// TODO: should probably come up with a policy vs. not force feeding this functionality
try {
write(buildStateMachineContext(stateMachine, state), (T)stateMachine.getId());
write(buildStateMachineContext(stateMachine, rootStateMachine, state), (T)stateMachine.getId());
} catch (Exception e) {
throw new StateMachineException("Unable to persist stateMachineContext", e);
}
}
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {
preStateChange(state, message, transition, stateMachine, stateMachine);
}
@SuppressWarnings("unchecked")
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition, StateMachine<S, E> stateMachine) {
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
if (log.isDebugEnabled()) {
log.debug("postStateChange with stateMachine " + stateMachine);
log.debug("postStateChange with root stateMachine " + rootStateMachine);
log.debug("postStateChange with state " + state);
}
// initial transitions are never intercepted as those cannot fail or get aborted.
// for now, handle persistence in post state change
// TODO: consider intercept initial transition, but not aborting if error is thrown?
if (state != null && transition != null && transition.getKind() == TransitionKind.INITIAL) {
try {
write(buildStateMachineContext(stateMachine, state), (T)stateMachine.getId());
write(buildStateMachineContext(stateMachine, rootStateMachine, state), (T)stateMachine.getId());
} catch (Exception e) {
throw new StateMachineException("Unable to persist stateMachineContext", e);
}
}
}
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {
postStateChange(state, message, transition, stateMachine, stateMachine);
}
/**
* Write {@link StateMachineContext} into persistent store.
*
@@ -119,22 +147,27 @@ public abstract class AbstractPersistingStateMachineInterceptor<S, E, T> extends
* Builds the state machine context.
*
* @param stateMachine the state machine
* @param rootStateMachine the root state machine
* @param state the state
* @return the state machine context
*/
protected StateMachineContext<S, E> buildStateMachineContext(StateMachine<S, E> stateMachine, State<S, E> state) {
protected StateMachineContext<S, E> buildStateMachineContext(StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine, State<S, E> state) {
ExtendedState extendedState = new DefaultExtendedState();
extendedState.getVariables().putAll(extendedStateVariablesFunction.apply(stateMachine));
ArrayList<StateMachineContext<S, E>> childs = new ArrayList<StateMachineContext<S, E>>();
List<StateMachineContext<S, E>> childs = new ArrayList<StateMachineContext<S, E>>();
List<String> childRefs = new ArrayList<>();
S id = null;
if (state.isSubmachineState()) {
id = getDeepState(state);
} else if (state.isOrthogonal()) {
Collection<Region<S, E>> regions = ((AbstractState<S, E>)state).getRegions();
for (Region<S, E> r : regions) {
StateMachine<S, E> rsm = (StateMachine<S, E>) r;
childs.add(buildStateMachineContext(rsm, state));
if (stateMachine.getState().isOrthogonal()) {
Collection<Region<S, E>> regions = ((AbstractState<S, E>)state).getRegions();
for (Region<S, E> r : regions) {
// realistically we can only add refs because reqions are independent
// and when restoring, those child contexts need to get dehydrated
childRefs.add(r.getId());
}
}
id = state.getId();
} else {
@@ -160,7 +193,7 @@ public abstract class AbstractPersistingStateMachineInterceptor<S, E, T> extends
}
}
}
return new DefaultStateMachineContext<S, E>(childs, id, null, null, extendedState, historyStates, stateMachine.getId());
return new DefaultStateMachineContext<S, E>(childRefs, childs, id, null, null, extendedState, historyStates, stateMachine.getId());
}
private S getDeepState(State<S, E> state) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 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.
@@ -152,7 +152,8 @@ public class DefaultStateMachineService<S, E> implements StateMachineService<S,
return stateMachine;
}
stateMachine.stop();
stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<S, E>>() {
// only go via top region
stateMachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S, E>>() {
@Override
public void apply(StateMachineAccess<S, E> function) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2018 the original author or authors.
* Copyright 2015-2019 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.
@@ -67,6 +67,7 @@ import org.springframework.statemachine.transition.TransitionKind;
import org.springframework.statemachine.trigger.DefaultTriggerContext;
import org.springframework.statemachine.trigger.Trigger;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -697,19 +698,23 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
}
stateSet = true;
break;
} else if (!stateMachineContext.getChilds().isEmpty()) {
} else if (stateMachineContext.getChilds() != null && !stateMachineContext.getChilds().isEmpty()) {
// we're here because root machine only have regions
if (s.isOrthogonal()) {
Collection<Region<S, E>> regions = ((AbstractState<S, E>)s).getRegions();
for (Region<S, E> region : regions) {
for (final StateMachineContext<S, E> child : stateMachineContext.getChilds()) {
((StateMachine<S, E>)region).getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S,E>>() {
// only call if reqion id matches with context id
if (ObjectUtils.nullSafeEquals(region.getId(), child.getId())) {
((StateMachine<S, E>)region).getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S,E>>() {
@Override
public void apply(StateMachineAccess<S, E> function) {
function.resetStateMachine(child);
}
});
@Override
public void apply(StateMachineAccess<S, E> function) {
function.resetStateMachine(child);
}
});
}
}
}
} else {
@@ -872,7 +877,7 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
private boolean callPreStateChangeInterceptors(State<S,E> state, Message<E> message, Transition<S,E> transition, StateMachine<S, E> stateMachine) {
try {
getStateMachineInterceptors().preStateChange(state, message, transition, stateMachine);
getStateMachineInterceptors().preStateChange(state, message, transition, this, stateMachine);
} catch (Exception e) {
log.info("Interceptors threw exception, skipping state change", e);
return false;
@@ -882,8 +887,9 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
private void callPostStateChangeInterceptors(State<S,E> state, Message<E> message, Transition<S,E> transition, StateMachine<S, E> stateMachine) {
try {
getStateMachineInterceptors().postStateChange(state, message, transition, stateMachine);
getStateMachineInterceptors().postStateChange(state, message, transition, this, stateMachine);
} catch (Exception e) {
log.warn("Interceptors threw exception in post state change", e);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-2019 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.
@@ -35,6 +35,7 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
private final String id;
private final List<StateMachineContext<S, E>> childs;
private final List<String> childRefs;
private final S state;
private final Map<S, S> historyStates;
private final E event;
@@ -125,6 +126,31 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
public DefaultStateMachineContext(List<StateMachineContext<S, E>> childs, S state, E event,
Map<String, Object> eventHeaders, ExtendedState extendedState, Map<S, S> historyStates, String id) {
this.childs = childs;
this.childRefs = null;
this.state = state;
this.event = event;
this.eventHeaders = eventHeaders;
this.extendedState = extendedState;
this.historyStates = historyStates != null ? historyStates : new HashMap<S, S>();
this.id = id;
}
/**
* Instantiates a new default state machine context.
*
* @param childRefs the child state machine context refs
* @param childs the child state machine contexts
* @param state the state
* @param event the event
* @param eventHeaders the event headers
* @param extendedState the extended state
* @param historyStates the history state mappings
* @param id the machine id
*/
public DefaultStateMachineContext(List<String> childRefs, List<StateMachineContext<S, E>> childs, S state, E event,
Map<String, Object> eventHeaders, ExtendedState extendedState, Map<S, S> historyStates, String id) {
this.childs = childs;
this.childRefs = childRefs;
this.state = state;
this.event = event;
this.eventHeaders = eventHeaders;
@@ -143,6 +169,11 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
return childs;
}
@Override
public List<String> getChildReferences() {
return childRefs;
}
@Override
public S getState() {
return state;
@@ -170,7 +201,8 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
@Override
public String toString() {
return "DefaultStateMachineContext [id=" + id + ", childs=" + childs + ", state=" + state + ", historyStates=" + historyStates
+ ", event=" + event + ", eventHeaders=" + eventHeaders + ", extendedState=" + extendedState + "]";
return "DefaultStateMachineContext [id=" + id + ", childs=" + childs + ", childRefs=" + childRefs + ", state="
+ state + ", historyStates=" + historyStates + ", event=" + event + ", eventHeaders=" + eventHeaders
+ ", extendedState=" + extendedState + "]";
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2019 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.
@@ -54,6 +54,19 @@ public interface StateMachineInterceptor<S, E> {
void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine);
/**
* Called prior of a state change. Throwing an exception
* from this method will stop a state change logic.
*
* @param state the state
* @param message the message
* @param transition the transition
* @param stateMachine the state machine
* @param rootStateMachine the root state machine
*/
void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine);
/**
* Called after a state change.
*
@@ -65,6 +78,18 @@ public interface StateMachineInterceptor<S, E> {
void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine);
/**
* Called after a state change.
*
* @param state the state
* @param message the message
* @param transition the transition
* @param stateMachine the state machine
* @param rootStateMachine the root state machine
*/
void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine);
/**
* Called prior of a start of a transition. Returning
* {@code null} from this method will break the transtion

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2019 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.
@@ -41,11 +41,21 @@ public class StateMachineInterceptorAdapter<S, E> implements StateMachineInterce
StateMachine<S, E> stateMachine) {
}
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
}
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine) {
}
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
}
@Override
public StateContext<S, E> preTransition(StateContext<S, E> stateContext) {
return stateContext;
@@ -60,5 +70,4 @@ public class StateMachineInterceptorAdapter<S, E> implements StateMachineInterce
public Exception stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
return exception;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2019 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.
@@ -102,6 +102,13 @@ public class StateMachineInterceptorList<S, E> {
}
}
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
for (StateMachineInterceptor<S, E> interceptor : interceptors) {
interceptor.preStateChange(state, message, transition, stateMachine, rootStateMachine);
}
}
/**
* Post state change.
*
@@ -117,6 +124,13 @@ public class StateMachineInterceptorList<S, E> {
}
}
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
StateMachine<S, E> stateMachine, StateMachine<S, E> rootStateMachine) {
for (StateMachineInterceptor<S, E> interceptor : interceptors) {
interceptor.postStateChange(state, message, transition, stateMachine, rootStateMachine);
}
}
/**
* Pre transition.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2018 the original author or authors.
* Copyright 2015-2019 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.
@@ -1294,6 +1294,12 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests {
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
}
@Override
public void preStateChange(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine,
StateMachine<String, String> rootStateMachine) {
}
@Override
public StateContext<String, String> postTransition(StateContext<String, String> stateContext) {
return stateContext;
@@ -1304,6 +1310,12 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests {
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
}
@Override
public void postStateChange(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine,
StateMachine<String, String> rootStateMachine) {
}
@Override
public Exception stateMachineError(StateMachine<String, String> stateMachine,
Exception exception) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2016-2019 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.
@@ -323,6 +323,39 @@ public class StateMachinePersistTests extends AbstractStateMachineTests {
assertThat(stateMachine.isComplete(), is(true));
}
@SuppressWarnings("unchecked")
@Test
public void testRegions2() throws Exception {
context.register(Config9.class);
context.refresh();
StateMachine<String, String> stateMachine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
stateMachine.start();
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S11", "S21", "S31"));
stateMachine.sendEvent("E1");
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S21", "S31"));
stateMachine.sendEvent("E2");
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S31"));
stateMachine.sendEvent("E3");
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S32"));
InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1();
StateMachinePersister<String, String, String> persister = new DefaultStateMachinePersister<>(stateMachinePersist);
persister.persist(stateMachine, "xxx");
stateMachine = persister.restore(stateMachine, "xxx");
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S32"));
stateMachine.sendEvent("E4");
stateMachine.sendEvent("E5");
stateMachine.sendEvent("E6");
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S13", "S23", "S33"));
stateMachine = persister.restore(stateMachine, "xxx");
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S32"));
}
@Configuration
@EnableStateMachine
static class Config1 extends StateMachineConfigurerAdapter<String, String> {
@@ -685,6 +718,67 @@ public class StateMachinePersistTests extends AbstractStateMachineTests {
}
}
@Configuration
@EnableStateMachine
static class Config9 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S11")
.state("S11")
.state("S12")
.state("S13")
.and()
.withStates()
.initial("S21")
.state("S21")
.state("S22")
.state("S23")
.and()
.withStates()
.initial("S31")
.state("S31")
.state("S32")
.state("S33");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S11")
.target("S12")
.event("E1")
.and()
.withExternal()
.source("S21")
.target("S22")
.event("E2")
.and()
.withExternal()
.source("S31")
.target("S32")
.event("E3")
.and()
.withExternal()
.source("S12")
.target("S13")
.event("E4")
.and()
.withExternal()
.source("S22")
.target("S23")
.event("E5")
.and()
.withExternal()
.source("S32")
.target("S33")
.event("E6");
}
}
static class InMemoryStateMachinePersist1 implements StateMachinePersist<String, String, String> {
private final HashMap<String, StateMachineContext<String, String>> contexts = new HashMap<>();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2019 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.
@@ -619,6 +619,13 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests {
}
@Override
public void preStateChange(State<States, Events> state, Message<Events> message,
Transition<States, Events> transition, StateMachine<States, Events> stateMachine,
StateMachine<States, Events> rootStateMachine) {
preStateChange(state, message, transition, stateMachine);
}
@Override
public void postStateChange(State<States, Events> state, Message<Events> message,
Transition<States, Events> transition, StateMachine<States, Events> stateMachine) {
@@ -627,6 +634,13 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests {
postStateChangeLatch.countDown();
}
@Override
public void postStateChange(State<States, Events> state, Message<Events> message,
Transition<States, Events> transition, StateMachine<States, Events> stateMachine,
StateMachine<States, Events> rootStateMachine) {
postStateChange(state, message, transition, stateMachine);
}
@Override
public StateContext<States, Events> preTransition(StateContext<States, Events> stateContext) {
return stateContext;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 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.
@@ -63,7 +63,7 @@ public class JpaRepositoryStateMachinePersist<S, E> extends RepositoryStateMachi
protected JpaRepositoryStateMachine build(StateMachineContext<S, E> context, Object contextObj, byte[] serialisedContext) {
JpaRepositoryStateMachine jpaRepositoryStateMachine = new JpaRepositoryStateMachine();
jpaRepositoryStateMachine.setMachineId(context.getId());
jpaRepositoryStateMachine.setState(context.getState().toString());
jpaRepositoryStateMachine.setState(context.getState() != null ? context.getState().toString() : null);
jpaRepositoryStateMachine.setStateMachineContext(serialisedContext);
return jpaRepositoryStateMachine;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2018 the original author or authors.
* Copyright 2016-2019 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.
@@ -16,12 +16,15 @@
package org.springframework.statemachine.data.jpa;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertThat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -314,6 +317,30 @@ public class JpaRepositoryTests extends AbstractRepositoryTests {
assertThat(stateMachine.getState().getId(), is(PersistTestStates.S1));
}
@Test
@SuppressWarnings("unchecked")
public void testStateMachinePersistWithRootRegions() {
context.register(TestConfig.class, ConfigWithRootRegions.class);
context.refresh();
JpaStateMachineRepository stateMachineRepository = context.getBean(JpaStateMachineRepository.class);
StateMachine<String, String> stateMachine = context.getBean(StateMachine.class);
stateMachine.start();
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S10", "S20"));
stateMachine.sendEvent("E1");
assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S11", "S21"));
assertThat(stateMachineRepository.count(), is(3l));
List<String> ids = StreamSupport.stream(stateMachineRepository.findAll().spliterator(), false)
.map(jrsm -> jrsm.getMachineId()).collect(Collectors.toList());
assertThat(ids.size(), is(3));
// [null#238e8cc0-a932-4583-b696-2c057e5ebefe, null#486e20be-853e-4e4d-9a68-c62c061469ef, testid]
assertThat(ids, containsInAnyOrder("testid", "testid#R1", "testid#R2"));
}
@EnableAutoConfiguration
static class TestConfig {
}
@@ -442,4 +469,57 @@ public class JpaRepositoryTests extends AbstractRepositoryTests {
public enum PersistTestEvents {
E1, E2;
}
@Configuration
@EnableStateMachine
static class ConfigWithRootRegions extends StateMachineConfigurerAdapter<String, String> {
@Autowired
private JpaStateMachineRepository jpaStateMachineRepository;
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config
.withConfiguration()
.machineId("testid")
.and()
.withPersistence()
.runtimePersister(stateMachineRuntimePersister());
}
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.region("R1")
.initial("S10")
.state("S10")
.state("S11")
.and()
.withStates()
.region("R2")
.initial("S20")
.state("S20")
.state("S21");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S10")
.target("S11")
.event("E1")
.and()
.withExternal()
.source("S20")
.target("S21")
.event("E1");
}
@Bean
public StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister() {
return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2018 the original author or authors.
* Copyright 2017-2019 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.
@@ -15,12 +15,16 @@
*/
package org.springframework.statemachine.data;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.kryo.KryoStateMachineSerialisationService;
import org.springframework.statemachine.service.StateMachineSerialisationService;
import org.springframework.statemachine.support.DefaultStateMachineContext;
import org.springframework.util.Assert;
/**
@@ -66,8 +70,27 @@ public abstract class RepositoryStateMachinePersist<M extends RepositoryStateMac
@Override
public StateMachineContext<S, E> read(Object contextObj) throws Exception {
M repositoryStateMachine = getRepository().findById(contextObj.toString()).orElse(null);
// use child contexts if we have those, otherwise fall back to child context refs.
if (repositoryStateMachine != null) {
return serialisationService.deserialiseStateMachineContext(repositoryStateMachine.getStateMachineContext());
StateMachineContext<S, E> context = serialisationService
.deserialiseStateMachineContext(repositoryStateMachine.getStateMachineContext());;
if (context != null && context.getChilds() != null && context.getChilds().isEmpty()
&& context.getChildReferences() != null) {
List<StateMachineContext<S, E>> contexts = new ArrayList<>();
for (String childRef : context.getChildReferences()) {
repositoryStateMachine = getRepository().findById(childRef).orElse(null);
if (repositoryStateMachine != null) {
contexts.add(serialisationService
.deserialiseStateMachineContext(repositoryStateMachine.getStateMachineContext()));
}
}
return new DefaultStateMachineContext<S, E>(contexts, context.getState(), context.getEvent(),
context.getEventHeaders(), context.getExtendedState(), context.getHistoryStates(),
context.getId());
} else {
return context;
}
}
return null;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2018 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.
@@ -23,6 +23,7 @@ import java.io.OutputStream;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.service.StateMachineSerialisationService;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
@@ -49,6 +50,10 @@ public abstract class AbstractKryoStateMachineSerialisationService<S, E> impleme
@Override
public Kryo create() {
Kryo kryo = new Kryo();
// kryo is really getting trouble checking things if class loaders
// doesn't match. for now just use below trick before we try
// to go fully on beans and get a bean class loader.
kryo.setClassLoader(ClassUtils.getDefaultClassLoader());
configureKryoInstance(kryo);
return kryo;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2019 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.
@@ -46,6 +46,10 @@ public class StateMachineContextSerializer<S, E> extends Serializer<StateMachine
kryo.writeClassAndObject(output, context.getChilds());
kryo.writeClassAndObject(output, context.getHistoryStates());
kryo.writeClassAndObject(output, context.getId());
// child refs is added after initial implementation, leaving this not here
// in case it's starting to cause issues with any existing serialized contexts
// which doesn't have this field
kryo.writeClassAndObject(output, context.getChildReferences());
}
@SuppressWarnings("unchecked")
@@ -58,7 +62,8 @@ public class StateMachineContextSerializer<S, E> extends Serializer<StateMachine
List<StateMachineContext<S, E>> childs = (List<StateMachineContext<S, E>>) kryo.readClassAndObject(input);
Map<S, S> historyStates = (Map<S, S>) kryo.readClassAndObject(input);
String id = (String) kryo.readClassAndObject(input);
return new DefaultStateMachineContext<S, E>(childs, state, event, eventHeaders, new DefaultExtendedState(variables), historyStates, id);
List<String> childRefs = (List<String>) kryo.readClassAndObject(input);
return new DefaultStateMachineContext<S, E>(childRefs, childs, state, event, eventHeaders,
new DefaultExtendedState(variables), historyStates, id);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2018 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.kryo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.support.DefaultExtendedState;
import org.springframework.statemachine.support.DefaultStateMachineContext;
/**
* Tests for {@link KryoStateMachineSerialisationService}.
*
* @author Janne Valkealahti
*
*/
public class KryoStateMachineSerialisationServiceTests {
@Test
public void testContextWithChilds() throws Exception {
StateMachineContext<String, String> child1 = new DefaultStateMachineContext<String, String>("child1", null, null,
new DefaultExtendedState());
StateMachineContext<String, String> child2 = new DefaultStateMachineContext<String, String>("child2", null, null,
new DefaultExtendedState());
List<StateMachineContext<String, String>> childs = new ArrayList<>();
childs.add(child1);
childs.add(child2);
StateMachineContext<String, String> root = new DefaultStateMachineContext<String, String>(childs, "root", null,
null, new DefaultExtendedState());
KryoStateMachineSerialisationService<String, String> service = new KryoStateMachineSerialisationService<>();
byte[] bytes = service.serialiseStateMachineContext(root);
StateMachineContext<String, String> context = service.deserialiseStateMachineContext(bytes);
assertThat(context.getChilds().size(), is(2));
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2018 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.kryo;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.support.DefaultExtendedState;
import org.springframework.statemachine.support.DefaultStateMachineContext;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
/**
* Tests for {@link StateMachineContextSerializer}.
*
* @author Janne Valkealahti
*
*/
public class StateMachineContextSerializerTests {
private Kryo kryo;
private Output output;
private Input input;
@Before
public void setUp() throws Exception {
kryo = new Kryo();
}
@Test
public void testContextWithChilds() {
StateMachineContextSerializer<String, String> serializer = new StateMachineContextSerializer<>();
kryo.addDefaultSerializer(StateMachineContext.class, serializer);
StateMachineContext<String, String> child = new DefaultStateMachineContext<String, String>(new ArrayList<>(), "child", "event1",
new HashMap<String, Object>(), new DefaultExtendedState());
List<StateMachineContext<String, String>> childs = new ArrayList<>();
childs.add(child);
StateMachineContext<String, String> root = new DefaultStateMachineContext<String, String>(childs, "root", "event2",
new HashMap<String, Object>(), new DefaultExtendedState());
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
output = new Output(outStream);
kryo.writeClassAndObject(output, root);
output.flush();
input = new Input(new ByteArrayInputStream(outStream.toByteArray()));
kryo.readClassAndObject(input);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2019 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.
@@ -120,6 +120,13 @@ public class PersistStateMachineHandler extends LifecycleObjectSupport {
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
listeners.onPersist(state, message, transition, stateMachine);
}
@Override
public void preStateChange(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine,
StateMachine<String, String> rootStateMachine) {
listeners.onPersist(state, message, transition, stateMachine);
}
}
private class CompositePersistStateChangeListener extends AbstractCompositeListener<PersistStateChangeListener> implements

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2019 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.
@@ -878,6 +878,13 @@ public class TasksHandler {
throw new StateMachineException("Error persisting", e);
}
}
@Override
public void preStateChange(State<String, String> state, Message<String> message,
Transition<String, String> transition, StateMachine<String, String> stateMachine,
StateMachine<String, String> rootStateMachine) {
preStateChange(state, message, transition, stateMachine);
}
}
/**

View File

@@ -128,6 +128,19 @@ project('spring-statemachine-samples-datajpa') {
}
}
project('spring-statemachine-samples-datajpamultipersist') {
description = 'Spring State Machine Data Jpa Multi Persist Sample'
dependencies {
compile project(":spring-statemachine-autoconfigure")
compile project(":spring-statemachine-data-common:spring-statemachine-data-jpa")
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile("org.springframework.boot:spring-boot-devtools")
compile("com.h2database:h2")
}
}
project('spring-statemachine-samples-datapersist') {
description = 'Spring State Machine Data Persist Sample'
dependencies {

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2018 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.datajpamultipersist;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//tag::snippetA[]
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
//end::snippetA[]

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2018 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.datajpamultipersist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineModelConfigurer;
import org.springframework.statemachine.config.model.StateMachineModelFactory;
import org.springframework.statemachine.data.RepositoryState;
import org.springframework.statemachine.data.RepositoryStateMachineModelFactory;
import org.springframework.statemachine.data.RepositoryTransition;
import org.springframework.statemachine.data.StateRepository;
import org.springframework.statemachine.data.TransitionRepository;
import org.springframework.statemachine.data.jpa.JpaPersistingStateMachineInterceptor;
import org.springframework.statemachine.data.jpa.JpaStateMachineRepository;
import org.springframework.statemachine.data.support.StateMachineJackson2RepositoryPopulatorFactoryBean;
import org.springframework.statemachine.persist.StateMachineRuntimePersister;
import org.springframework.statemachine.service.DefaultStateMachineService;
import org.springframework.statemachine.service.StateMachineService;
@Configuration
public class StateMachineConfig {
@Bean
public StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister(
JpaStateMachineRepository jpaStateMachineRepository) {
return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
@Bean
public StateMachineService<String, String> stateMachineService(
StateMachineFactory<String, String> stateMachineFactory,
StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister) {
return new DefaultStateMachineService<String, String>(stateMachineFactory, stateMachineRuntimePersister);
}
@Bean
public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
factoryBean.setResources(new Resource[] { new ClassPathResource("data.json") });
return factoryBean;
}
@Configuration
@EnableStateMachineFactory
public static class Config extends StateMachineConfigurerAdapter<String, String> {
@Autowired
private StateRepository<? extends RepositoryState> stateRepository;
@Autowired
private TransitionRepository<? extends RepositoryTransition> transitionRepository;
@Autowired
private StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister;
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withPersistence()
.runtimePersister(stateMachineRuntimePersister);
}
@Override
public void configure(StateMachineModelConfigurer<String, String> model)
throws Exception {
model
.withModel()
.factory(modelFactory());
}
@Bean
public StateMachineModelFactory<String, String> modelFactory() {
return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
}
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2019 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.datajpamultipersist;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.data.RepositoryTransition;
import org.springframework.statemachine.data.TransitionRepository;
import org.springframework.statemachine.service.StateMachineService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class StateMachineController {
public final static String MACHINE_ID_1 = "datajpamultipersist1";
public final static String MACHINE_ID_2 = "datajpamultipersist2";
public final static String MACHINE_ID_2R1 = "datajpamultipersist2#R1";
public final static String MACHINE_ID_2R2 = "datajpamultipersist2#R2";
private final static String[] MACHINES = new String[] { MACHINE_ID_1, MACHINE_ID_2 };
private final StateMachineLogListener listener = new StateMachineLogListener();
@Autowired
private StateMachineService<String, String> stateMachineService;
@Autowired
private StateMachinePersist<String, String, String> stateMachinePersist;
@Autowired
private TransitionRepository<? extends RepositoryTransition> transitionRepository;
private StateMachine<String, String> currentStateMachine;
@RequestMapping("/")
public String home() {
return "redirect:/state";
}
@RequestMapping("/state")
public String feedAndGetStates(
@RequestParam(value = "events", required = false) List<String> events,
@RequestParam(value = "machine", required = false, defaultValue = MACHINE_ID_1) String machine,
Model model) throws Exception {
StateMachine<String, String> stateMachine = getStateMachine(machine);
if (events != null) {
for (String event : events) {
stateMachine.sendEvent(event);
}
}
StringBuilder contextBuf = new StringBuilder();
StateMachineContext<String, String> stateMachineContext = stateMachinePersist.read(machine);
if (stateMachineContext != null) {
contextBuf.append(stateMachineContext.toString());
}
if (ObjectUtils.nullSafeEquals(machine, MACHINE_ID_2)) {
stateMachineContext = stateMachinePersist.read(MACHINE_ID_2R1);
if (stateMachineContext != null) {
contextBuf.append("\n---\n");
contextBuf.append(stateMachineContext.toString());
}
stateMachineContext = stateMachinePersist.read(MACHINE_ID_2R2);
if (stateMachineContext != null) {
contextBuf.append("\n---\n");
contextBuf.append(stateMachineContext.toString());
}
}
model.addAttribute("allMachines", MACHINES);
model.addAttribute("machine", machine);
model.addAttribute("currentMachine", currentStateMachine);
model.addAttribute("allEvents", getEvents());
model.addAttribute("messages", createMessages(listener.getMessages()));
model.addAttribute("context", contextBuf.toString());
return "states";
}
private synchronized StateMachine<String, String> getStateMachine(String machineId) throws Exception {
listener.resetMessages();
if (currentStateMachine == null) {
currentStateMachine = stateMachineService.acquireStateMachine(machineId, false);
currentStateMachine.addStateListener(listener);
currentStateMachine.start();
} else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
stateMachineService.releaseStateMachine(currentStateMachine.getId());
currentStateMachine.stop();
currentStateMachine = stateMachineService.acquireStateMachine(machineId, false);
currentStateMachine.addStateListener(listener);
currentStateMachine.start();
}
return currentStateMachine;
}
private String[] getEvents() {
List<String> events = new ArrayList<>();
for (RepositoryTransition t : transitionRepository.findAll()) {
events.add(t.getEvent());
}
return events.toArray(new String[0]);
}
private String createMessages(List<String> messages) {
StringBuilder buf = new StringBuilder();
for (String message : messages) {
buf.append(message);
buf.append("\n");
}
return buf.toString();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2018 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.datajpamultipersist;
import java.util.LinkedList;
import java.util.List;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateContext.Stage;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
public class StateMachineLogListener extends StateMachineListenerAdapter<String, String> {
private final LinkedList<String> messages = new LinkedList<String>();
public List<String> getMessages() {
return messages;
}
public void resetMessages() {
messages.clear();
}
@Override
public void stateContext(StateContext<String, String> stateContext) {
if (stateContext.getStage() == Stage.STATE_ENTRY) {
messages.addFirst("Enter " + stateContext.getTarget().getId());
} else if (stateContext.getStage() == Stage.STATE_EXIT) {
messages.addFirst("Exit " + stateContext.getSource().getId());
} else if (stateContext.getStage() == Stage.STATEMACHINE_START) {
messages.addLast("Machine started");
} else if (stateContext.getStage() == Stage.STATEMACHINE_STOP) {
messages.addFirst("Machine stopped");
}
}
}

View File

@@ -0,0 +1,7 @@
logging:
level:
root: INFO
org.springframework.statemachine: DEBUG
security:
basic:
enabled: false

View File

@@ -0,0 +1,172 @@
[
{
"@id": "100",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello exit S1')"
},
{
"@id": "101",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello entry S2')"
},
{
"@id": "102",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello state S3')"
},
{
"@id": "103",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello')"
},
{
"@id": "10",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist1",
"initial": true,
"state": "S1",
"exitActions": ["100"]
},
{
"@id": "11",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist1",
"initial": false,
"state": "S2",
"entryActions": ["101"]
},
{
"@id": "12",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist1",
"initial": false,
"state": "S3",
"stateActions": ["102"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist1",
"source": "10",
"target": "11",
"event": "E1",
"kind": "EXTERNAL"
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist1",
"source": "11",
"target": "12",
"event": "E2",
"actions": ["103"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist1",
"source": "12",
"target": "11",
"event": "E3",
"actions": ["103"]
},
{
"@id": "20",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R1",
"initial": true,
"state": "S10",
"exitActions": ["100"]
},
{
"@id": "21",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R1",
"initial": false,
"state": "S11",
"entryActions": ["101"]
},
{
"@id": "22",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R1",
"initial": false,
"state": "S12",
"stateActions": ["102"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "20",
"target": "21",
"event": "E10",
"kind": "EXTERNAL"
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "21",
"target": "22",
"event": "E11",
"actions": ["103"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "22",
"target": "21",
"event": "E12",
"actions": ["103"]
},
{
"@id": "30",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R2",
"initial": true,
"state": "S20",
"exitActions": ["100"]
},
{
"@id": "31",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R2",
"initial": false,
"state": "S21",
"entryActions": ["101"]
},
{
"@id": "32",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R2",
"initial": false,
"state": "S22",
"stateActions": ["102"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "30",
"target": "31",
"event": "E20",
"kind": "EXTERNAL"
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "31",
"target": "32",
"event": "E21",
"actions": ["103"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "32",
"target": "31",
"event": "E22",
"actions": ["103"]
}
]

View File

@@ -0,0 +1,52 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Statemachine Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div>
<a href="/h2-console" target="_blank">h2 console</a>
</div>
<form action="#" data-th-action="@{/state}" data-th-object="${model}" method="post">
<div>
<p th:text="'Choose machine'"/>
<ul>
<li th:each="ty : ${allMachines}">
<input type="radio" name="machine" th:value="${ty}" th:checked="${ty} == ${machine}"/>
<label th:text="${ty}">replaced</label>
</li>
</ul>
</div>
<div>
<p th:text="'Choose events'"/>
<ul>
<li th:each="ty : ${allEvents}">
<input type="checkbox" name="events" th:value="${ty}" />
<label th:text="${ty}">replaced</label>
</li>
</ul>
</div>
<button type="submit">Send Events</button>
</form>
<div>
<h3>Events</h3>
</div>
<div>
<textarea th:text="${messages}" rows="15" cols="100"/>
</div>
<div>
<h3>StateMachineContext</h3>
</div>
<div>
<textarea th:text="${context}" rows="15" cols="100"/>
</div>
<div>
<h3>Current Machine</h3>
</div>
<div>
<textarea th:text="${currentMachine}" rows="10" cols="100"/>
</div>
</body>
</html>

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2019 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.datajpamultipersist;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import demo.datajpamultipersist.Application;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
@WebAppConfiguration
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class DataJpaMultiPersistTests {
private MockMvc mvc;
@Autowired
private WebApplicationContext context;
@Test
public void testHome() throws Exception {
mvc.
perform(get("/state")).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S1"),
containsString("Machine started"))));
}
@Test
public void testSendEventE1WithMachine1() throws Exception {
mvc.
perform(get("/state")
.param("events", "E1")
.param("machine", StateMachineController.MACHINE_ID_1)).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S1"),
containsString("Exit S1"),
containsString("Enter S2"))));
}
@Test
public void testSendEventsE1E2WithMachine1() throws Exception {
mvc.
perform(get("/state")
.param("events", "E1")
.param("events", "E2")
.param("machine", StateMachineController.MACHINE_ID_1)).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S1"),
containsString("Exit S1"),
containsString("Enter S2"),
containsString("Exit S2"),
containsString("Enter S3"))));
}
@Test
public void testWithMachine2() throws Exception {
mvc.
perform(get("/state")
.param("machine", StateMachineController.MACHINE_ID_2)).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S10"),
containsString("Enter S20"),
containsString("Enter null"))));
}
@Test
public void testSendEventsE10E20WithMachine2() throws Exception {
mvc.
perform(get("/state")
.param("events", "E10")
.param("events", "E20")
.param("machine", StateMachineController.MACHINE_ID_2)).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S10"),
containsString("Enter S20"),
containsString("Enter S11"),
containsString("Enter S21"),
containsString("Enter null"))));
}
@Before
public void setup() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(context).build();
}
}