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

+
+
+