From 84ca0aec3ec1e05e0ab2467fafa801f40893531a Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sat, 12 Jan 2019 08:36:51 +0000 Subject: [PATCH] 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 --- settings.gradle | 1 + .../statemachine/StateMachineContext.java | 9 +- .../config/AbstractStateMachineFactory.java | 50 ++++- .../configurers/DefaultStateConfigurer.java | 8 +- .../config/configurers/StateConfigurer.java | 8 + .../ensemble/DistributedStateMachine.java | 13 +- ...ractPersistingStateMachineInterceptor.java | 57 ++++-- .../service/DefaultStateMachineService.java | 5 +- .../support/AbstractStateMachine.java | 26 ++- .../support/DefaultStateMachineContext.java | 38 +++- .../support/StateMachineInterceptor.java | 27 ++- .../StateMachineInterceptorAdapter.java | 13 +- .../support/StateMachineInterceptorList.java | 16 +- .../docs/DocsConfigurationSampleTests.java | 14 +- .../persist/StateMachinePersistTests.java | 96 +++++++++- .../support/StateChangeInterceptorTests.java | 16 +- .../jpa/JpaRepositoryStateMachinePersist.java | 4 +- .../data/jpa/JpaRepositoryTests.java | 82 ++++++++- .../data/RepositoryStateMachinePersist.java | 27 ++- ...tKryoStateMachineSerialisationService.java | 7 +- .../kryo/StateMachineContextSerializer.java | 11 +- ...StateMachineSerialisationServiceTests.java | 55 ++++++ .../StateMachineContextSerializerTests.java | 71 ++++++++ .../persist/PersistStateMachineHandler.java | 9 +- .../recipes/tasks/TasksHandler.java | 9 +- spring-statemachine-samples/build.gradle | 13 ++ .../demo/datajpamultipersist/Application.java | 29 +++ .../StateMachineConfig.java | 98 ++++++++++ .../StateMachineController.java | 135 ++++++++++++++ .../StateMachineLogListener.java | 49 +++++ .../src/main/resources/application.yml | 7 + .../src/main/resources/data.json | 172 ++++++++++++++++++ .../src/main/resources/templates/states.html | 52 ++++++ .../DataJpaMultiPersistTests.java | 121 ++++++++++++ 34 files changed, 1297 insertions(+), 51 deletions(-) create mode 100644 spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationServiceTests.java create mode 100644 spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/StateMachineContextSerializerTests.java create mode 100644 spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/Application.java create mode 100644 spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineConfig.java create mode 100644 spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineController.java create mode 100644 spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineLogListener.java create mode 100644 spring-statemachine-samples/datajpamultipersist/src/main/resources/application.yml create mode 100644 spring-statemachine-samples/datajpamultipersist/src/main/resources/data.json create mode 100644 spring-statemachine-samples/datajpamultipersist/src/main/resources/templates/states.html create mode 100644 spring-statemachine-samples/datajpamultipersist/src/test/java/demo/datajpamultipersist/DataJpaMultiPersistTests.java diff --git a/settings.gradle b/settings.gradle index c54e00bc..23e30b58 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContext.java index 4c4a958b..96c3253a 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContext.java @@ -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 { */ List> getChilds(); + /** + * Gets the child context references if any. + * + * @return the child context references + */ + List getChildReferences(); + /** * Gets the state. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java index b4b3c7a2..37e42bdc 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java @@ -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 extends LifecycleObjectS if (initialCount > 1) { for (Collection> 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(machine)); machines.add(machine); } @@ -359,10 +365,12 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS List> interceptors = stateMachineModel.getConfigurationData().getStateMachineInterceptors(); if (interceptors != null) { for (final StateMachineInterceptor interceptor : interceptors) { - machine.getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { + // add persisting interceptor hooks to all regions + RegionPersistingInterceptorAdapter adapter = new RegionPersistingInterceptorAdapter<>(interceptor, machine); + machine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction>() { @Override public void apply(StateMachineAccess function) { - function.addStateMachineInterceptor(interceptor); + function.addStateMachineInterceptor(adapter); } }); } @@ -377,6 +385,42 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS return delegateAutoStartup(machine); } + private static class RegionPersistingInterceptorAdapter extends StateMachineInterceptorAdapter { + + private final StateMachineInterceptor interceptor; + private final StateMachine rootStateMachine; + + public RegionPersistingInterceptorAdapter(StateMachineInterceptor interceptor, StateMachine rootStateMachine) { + this.interceptor = interceptor; + this.rootStateMachine = rootStateMachine; + } + + @Override + public void preStateChange(State state, Message message, Transition transition, + StateMachine stateMachine) { + interceptor.preStateChange(state, message, transition, stateMachine, rootStateMachine); + } + + @Override + public void preStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine) { + interceptor.preStateChange(state, message, transition, stateMachine, rootStateMachine); + } + + @Override + public void postStateChange(State state, Message message, Transition transition, + StateMachine stateMachine) { + interceptor.postStateChange(state, message, transition, stateMachine, rootStateMachine); + } + + @Override + public void postStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine) { + interceptor.postStateChange(state, message, transition, stateMachine, rootStateMachine); + } + + } + /** * Instructs this factory to handle auto-start flag manually * by calling lifecycle start method. diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java index 133b6443..f53bd862 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java @@ -47,7 +47,7 @@ public class DefaultStateConfigurer implements StateConfigurer { private Object parent; - private final Object region = UUID.randomUUID().toString(); + private Object region = UUID.randomUUID().toString(); private final Map> incomplete = new HashMap>(); private S initialState; private Action initialAction; @@ -123,6 +123,12 @@ public class DefaultStateConfigurer return this; } + @Override + public StateConfigurer region(String id) { + this.region = id; + return this; + } + @Override public StateConfigurer end(S end) { this.ends.add(end); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java index 4571f850..7748a9c9 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java @@ -63,6 +63,14 @@ public interface StateConfigurer extends */ StateConfigurer parent(S state); + /** + * Specify a region for these states configured by this configurer instance. + * + * @param id the region id + * @return configurer for chaining + */ + StateConfigurer region(String id); + /** * Specify a state {@code S}. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java index 81385b94..366acd99 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/ensemble/DistributedStateMachine.java @@ -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 extends LifecycleObjectSupport implem } } + @Override + public void preStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine) { + preStateChange(state, message, transition, stateMachine); + } + @Override public void postStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { } + @Override + public void postStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine) { + } + @Override public StateContext preTransition(StateContext stateContext) { return stateContext; diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java index cd4d89de..37187c7a 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java @@ -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 extends StateMachineInterceptorAdapter implements StateMachinePersist { + private static final Log log = LogFactory.getLog(AbstractPersistingStateMachineInterceptor.class); private Function, Map> extendedStateVariablesFunction = new AllVariablesFunction<>(); @SuppressWarnings("unchecked") @Override - public void preStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { + public void preStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine 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 state, Message message, Transition transition, + StateMachine stateMachine) { + preStateChange(state, message, transition, stateMachine, stateMachine); + } + @SuppressWarnings("unchecked") @Override - public void postStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { + public void postStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine 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 state, Message message, Transition transition, + StateMachine stateMachine) { + postStateChange(state, message, transition, stateMachine, stateMachine); + } + /** * Write {@link StateMachineContext} into persistent store. * @@ -119,22 +147,27 @@ public abstract class AbstractPersistingStateMachineInterceptor 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 buildStateMachineContext(StateMachine stateMachine, State state) { + protected StateMachineContext buildStateMachineContext(StateMachine stateMachine, StateMachine rootStateMachine, State state) { ExtendedState extendedState = new DefaultExtendedState(); extendedState.getVariables().putAll(extendedStateVariablesFunction.apply(stateMachine)); - ArrayList> childs = new ArrayList>(); + List> childs = new ArrayList>(); + List childRefs = new ArrayList<>(); S id = null; if (state.isSubmachineState()) { id = getDeepState(state); } else if (state.isOrthogonal()) { - Collection> regions = ((AbstractState)state).getRegions(); - for (Region r : regions) { - StateMachine rsm = (StateMachine) r; - childs.add(buildStateMachineContext(rsm, state)); + if (stateMachine.getState().isOrthogonal()) { + Collection> regions = ((AbstractState)state).getRegions(); + for (Region 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 extends } } } - return new DefaultStateMachineContext(childs, id, null, null, extendedState, historyStates, stateMachine.getId()); + return new DefaultStateMachineContext(childRefs, childs, id, null, null, extendedState, historyStates, stateMachine.getId()); } private S getDeepState(State state) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/DefaultStateMachineService.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/DefaultStateMachineService.java index 77f5305e..3bd42519 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/DefaultStateMachineService.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/DefaultStateMachineService.java @@ -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 implements StateMachineService>() { + // only go via top region + stateMachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { @Override public void apply(StateMachineAccess function) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index cceb3f7a..b47a24ac 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -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 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> regions = ((AbstractState)s).getRegions(); + for (Region region : regions) { for (final StateMachineContext child : stateMachineContext.getChilds()) { - ((StateMachine)region).getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { + // only call if reqion id matches with context id + if (ObjectUtils.nullSafeEquals(region.getId(), child.getId())) { + ((StateMachine)region).getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { - @Override - public void apply(StateMachineAccess function) { - function.resetStateMachine(child); - } - }); + @Override + public void apply(StateMachineAccess function) { + function.resetStateMachine(child); + } + }); + } } } } else { @@ -872,7 +877,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo private boolean callPreStateChangeInterceptors(State state, Message message, Transition transition, StateMachine 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 extends StateMachineObjectSuppo private void callPostStateChangeInterceptors(State state, Message message, Transition transition, StateMachine 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); } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineContext.java index fbc812fc..0bd5a909 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineContext.java @@ -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 implements StateMachineContext> childs; + private final List childRefs; private final S state; private final Map historyStates; private final E event; @@ -125,6 +126,31 @@ public class DefaultStateMachineContext implements StateMachineContext> childs, S state, E event, Map eventHeaders, ExtendedState extendedState, Map 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(); + 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 childRefs, List> childs, S state, E event, + Map eventHeaders, ExtendedState extendedState, Map 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 implements StateMachineContext getChildReferences() { + return childRefs; + } + @Override public S getState() { return state; @@ -170,7 +201,8 @@ public class DefaultStateMachineContext implements StateMachineContext { void preStateChange(State state, Message message, Transition transition, StateMachine 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 state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine); + /** * Called after a state change. * @@ -65,6 +78,18 @@ public interface StateMachineInterceptor { void postStateChange(State state, Message message, Transition transition, StateMachine 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 state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine); + /** * Called prior of a start of a transition. Returning * {@code null} from this method will break the transtion diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorAdapter.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorAdapter.java index 1b6e22da..1452894e 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorAdapter.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorAdapter.java @@ -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 implements StateMachineInterce StateMachine stateMachine) { } + @Override + public void preStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine) { + } + @Override public void postStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { } + @Override + public void postStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine) { + } + @Override public StateContext preTransition(StateContext stateContext) { return stateContext; @@ -60,5 +70,4 @@ public class StateMachineInterceptorAdapter implements StateMachineInterce public Exception stateMachineError(StateMachine stateMachine, Exception exception) { return exception; } - } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorList.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorList.java index 41538ea5..0aa84a54 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorList.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineInterceptorList.java @@ -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 { } } + public void preStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine) { + for (StateMachineInterceptor interceptor : interceptors) { + interceptor.preStateChange(state, message, transition, stateMachine, rootStateMachine); + } + } + /** * Post state change. * @@ -117,6 +124,13 @@ public class StateMachineInterceptorList { } } + public void postStateChange(State state, Message message, Transition transition, + StateMachine stateMachine, StateMachine rootStateMachine) { + for (StateMachineInterceptor interceptor : interceptors) { + interceptor.postStateChange(state, message, transition, stateMachine, rootStateMachine); + } + } + /** * Pre transition. * diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java index 8c62a63d..e610df8b 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests.java @@ -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 transition, StateMachine stateMachine) { } + @Override + public void preStateChange(State state, Message message, + Transition transition, StateMachine stateMachine, + StateMachine rootStateMachine) { + } + @Override public StateContext postTransition(StateContext stateContext) { return stateContext; @@ -1304,6 +1310,12 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests { Transition transition, StateMachine stateMachine) { } + @Override + public void postStateChange(State state, Message message, + Transition transition, StateMachine stateMachine, + StateMachine rootStateMachine) { + } + @Override public Exception stateMachineError(StateMachine stateMachine, Exception exception) { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java index d53c0fcd..3039a400 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java @@ -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 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 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 { @@ -685,6 +718,67 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { } } + @Configuration + @EnableStateMachine + static class Config9 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer 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 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 { private final HashMap> contexts = new HashMap<>(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateChangeInterceptorTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateChangeInterceptorTests.java index 9c70a10b..fc9ac8d5 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateChangeInterceptorTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/support/StateChangeInterceptorTests.java @@ -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 state, Message message, + Transition transition, StateMachine stateMachine, + StateMachine rootStateMachine) { + preStateChange(state, message, transition, stateMachine); + } + @Override public void postStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { @@ -627,6 +634,13 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests { postStateChangeLatch.countDown(); } + @Override + public void postStateChange(State state, Message message, + Transition transition, StateMachine stateMachine, + StateMachine rootStateMachine) { + postStateChange(state, message, transition, stateMachine); + } + @Override public StateContext preTransition(StateContext stateContext) { return stateContext; diff --git a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java index 6baa903f..d55a4bcb 100644 --- a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java +++ b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java @@ -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 extends RepositoryStateMachi protected JpaRepositoryStateMachine build(StateMachineContext 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; } diff --git a/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java b/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java index 9f2d8d6e..131aaf11 100644 --- a/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java +++ b/spring-statemachine-data/jpa/src/test/java/org/springframework/statemachine/data/jpa/JpaRepositoryTests.java @@ -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 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 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 { + + @Autowired + private JpaStateMachineRepository jpaStateMachineRepository; + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .machineId("testid") + .and() + .withPersistence() + .runtimePersister(stateMachineRuntimePersister()); + } + + @Override + public void configure(StateMachineStateConfigurer 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 transitions) throws Exception { + transitions + .withExternal() + .source("S10") + .target("S11") + .event("E1") + .and() + .withExternal() + .source("S20") + .target("S21") + .event("E1"); + } + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister() { + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } + } } diff --git a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java index 8427db0d..42ff1d0c 100644 --- a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java +++ b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java @@ -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 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 context = serialisationService + .deserialiseStateMachineContext(repositoryStateMachine.getStateMachineContext());; + if (context != null && context.getChilds() != null && context.getChilds().isEmpty() + && context.getChildReferences() != null) { + List> 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(contexts, context.getState(), context.getEvent(), + context.getEventHeaders(), context.getExtendedState(), context.getHistoryStates(), + context.getId()); + } else { + return context; + } + } return null; } diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/AbstractKryoStateMachineSerialisationService.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/AbstractKryoStateMachineSerialisationService.java index c3a8e669..59e47286 100644 --- a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/AbstractKryoStateMachineSerialisationService.java +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/AbstractKryoStateMachineSerialisationService.java @@ -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 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; } diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/StateMachineContextSerializer.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/StateMachineContextSerializer.java index 5c7a7d8f..becf0d63 100644 --- a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/StateMachineContextSerializer.java +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/StateMachineContextSerializer.java @@ -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 extends Serializer extends Serializer> childs = (List>) kryo.readClassAndObject(input); Map historyStates = (Map) kryo.readClassAndObject(input); String id = (String) kryo.readClassAndObject(input); - return new DefaultStateMachineContext(childs, state, event, eventHeaders, new DefaultExtendedState(variables), historyStates, id); + List childRefs = (List) kryo.readClassAndObject(input); + return new DefaultStateMachineContext(childRefs, childs, state, event, eventHeaders, + new DefaultExtendedState(variables), historyStates, id); } - } diff --git a/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationServiceTests.java b/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationServiceTests.java new file mode 100644 index 00000000..585f9674 --- /dev/null +++ b/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/KryoStateMachineSerialisationServiceTests.java @@ -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 child1 = new DefaultStateMachineContext("child1", null, null, + new DefaultExtendedState()); + StateMachineContext child2 = new DefaultStateMachineContext("child2", null, null, + new DefaultExtendedState()); + List> childs = new ArrayList<>(); + childs.add(child1); + childs.add(child2); + StateMachineContext root = new DefaultStateMachineContext(childs, "root", null, + null, new DefaultExtendedState()); + + KryoStateMachineSerialisationService service = new KryoStateMachineSerialisationService<>(); + byte[] bytes = service.serialiseStateMachineContext(root); + + StateMachineContext context = service.deserialiseStateMachineContext(bytes); + assertThat(context.getChilds().size(), is(2)); + } +} diff --git a/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/StateMachineContextSerializerTests.java b/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/StateMachineContextSerializerTests.java new file mode 100644 index 00000000..e08b32b9 --- /dev/null +++ b/spring-statemachine-kryo/src/test/java/org/springframework/statemachine/kryo/StateMachineContextSerializerTests.java @@ -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 serializer = new StateMachineContextSerializer<>(); + kryo.addDefaultSerializer(StateMachineContext.class, serializer); + + StateMachineContext child = new DefaultStateMachineContext(new ArrayList<>(), "child", "event1", + new HashMap(), new DefaultExtendedState()); + List> childs = new ArrayList<>(); + childs.add(child); + StateMachineContext root = new DefaultStateMachineContext(childs, "root", "event2", + new HashMap(), 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); + } +} diff --git a/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/persist/PersistStateMachineHandler.java b/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/persist/PersistStateMachineHandler.java index 07749500..1f709781 100644 --- a/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/persist/PersistStateMachineHandler.java +++ b/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/persist/PersistStateMachineHandler.java @@ -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 transition, StateMachine stateMachine) { listeners.onPersist(state, message, transition, stateMachine); } + + @Override + public void preStateChange(State state, Message message, + Transition transition, StateMachine stateMachine, + StateMachine rootStateMachine) { + listeners.onPersist(state, message, transition, stateMachine); + } } private class CompositePersistStateChangeListener extends AbstractCompositeListener implements diff --git a/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/tasks/TasksHandler.java b/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/tasks/TasksHandler.java index bcf60f7c..8a71f44f 100644 --- a/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/tasks/TasksHandler.java +++ b/spring-statemachine-recipes/src/main/java/org/springframework/statemachine/recipes/tasks/TasksHandler.java @@ -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 state, Message message, + Transition transition, StateMachine stateMachine, + StateMachine rootStateMachine) { + preStateChange(state, message, transition, stateMachine); + } } /** diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index 3b75edd1..d83054cb 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -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 { diff --git a/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/Application.java b/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/Application.java new file mode 100644 index 00000000..18a2cec4 --- /dev/null +++ b/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/Application.java @@ -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[] diff --git a/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineConfig.java b/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineConfig.java new file mode 100644 index 00000000..0d52443d --- /dev/null +++ b/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineConfig.java @@ -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 stateMachineRuntimePersister( + JpaStateMachineRepository jpaStateMachineRepository) { + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } + + @Bean + public StateMachineService stateMachineService( + StateMachineFactory stateMachineFactory, + StateMachineRuntimePersister stateMachineRuntimePersister) { + return new DefaultStateMachineService(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 { + + @Autowired + private StateRepository stateRepository; + + @Autowired + private TransitionRepository transitionRepository; + + @Autowired + private StateMachineRuntimePersister stateMachineRuntimePersister; + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withPersistence() + .runtimePersister(stateMachineRuntimePersister); + } + + @Override + public void configure(StateMachineModelConfigurer model) + throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository); + } + } +} diff --git a/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineController.java b/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineController.java new file mode 100644 index 00000000..e8e3a673 --- /dev/null +++ b/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineController.java @@ -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 stateMachineService; + + @Autowired + private StateMachinePersist stateMachinePersist; + + @Autowired + private TransitionRepository transitionRepository; + + private StateMachine currentStateMachine; + + @RequestMapping("/") + public String home() { + return "redirect:/state"; + } + + @RequestMapping("/state") + public String feedAndGetStates( + @RequestParam(value = "events", required = false) List events, + @RequestParam(value = "machine", required = false, defaultValue = MACHINE_ID_1) String machine, + Model model) throws Exception { + + + StateMachine stateMachine = getStateMachine(machine); + if (events != null) { + for (String event : events) { + stateMachine.sendEvent(event); + } + } + + StringBuilder contextBuf = new StringBuilder(); + + StateMachineContext 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 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 events = new ArrayList<>(); + for (RepositoryTransition t : transitionRepository.findAll()) { + events.add(t.getEvent()); + } + return events.toArray(new String[0]); + } + + private String createMessages(List messages) { + StringBuilder buf = new StringBuilder(); + for (String message : messages) { + buf.append(message); + buf.append("\n"); + } + return buf.toString(); + } + +} diff --git a/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineLogListener.java b/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineLogListener.java new file mode 100644 index 00000000..1c54ea48 --- /dev/null +++ b/spring-statemachine-samples/datajpamultipersist/src/main/java/demo/datajpamultipersist/StateMachineLogListener.java @@ -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 { + + private final LinkedList messages = new LinkedList(); + + public List getMessages() { + return messages; + } + + public void resetMessages() { + messages.clear(); + } + + @Override + public void stateContext(StateContext 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"); + } + } +} diff --git a/spring-statemachine-samples/datajpamultipersist/src/main/resources/application.yml b/spring-statemachine-samples/datajpamultipersist/src/main/resources/application.yml new file mode 100644 index 00000000..e787bce1 --- /dev/null +++ b/spring-statemachine-samples/datajpamultipersist/src/main/resources/application.yml @@ -0,0 +1,7 @@ +logging: + level: + root: INFO + org.springframework.statemachine: DEBUG +security: + basic: + enabled: false diff --git a/spring-statemachine-samples/datajpamultipersist/src/main/resources/data.json b/spring-statemachine-samples/datajpamultipersist/src/main/resources/data.json new file mode 100644 index 00000000..a4f259f7 --- /dev/null +++ b/spring-statemachine-samples/datajpamultipersist/src/main/resources/data.json @@ -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"] + } +] diff --git a/spring-statemachine-samples/datajpamultipersist/src/main/resources/templates/states.html b/spring-statemachine-samples/datajpamultipersist/src/main/resources/templates/states.html new file mode 100644 index 00000000..68af86ff --- /dev/null +++ b/spring-statemachine-samples/datajpamultipersist/src/main/resources/templates/states.html @@ -0,0 +1,52 @@ + + + + Spring Statemachine Demo + + + + + +
+
+

+

    +
  • + + +
  • +
+
+
+

+

    +
  • + + +
  • +
+
+ +
+
+

Events

+
+
+