From 33a5370d014c05dacc50f1908dc953aa4e65cfb4 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sat, 18 Nov 2017 16:38:49 +0000 Subject: [PATCH] Preliminary support for Spring Data persist - Add new repository model for storing StateMachineContext via a new StateMachineRepository. - New StateMachineRuntimePersister interface to abstract needed functionality to do a runtime machine persistence. - As runtime persistence, as of now, is done via interceptors, define JpaRepositoryStateMachinePersist and JpaPersistingStateMachineInterceptor to define StateMachineRuntimePersister logic. - Add new datajpapersist sample demonstrating new concepts. - Keep tests related to jpa as there's not redis/mongo integration implemented in this first iteration. - As this is going to be WIP until features around this issues are completed, docs, etc are not yet added. Also, interfaces and impls are subject to change during a process. - Relates to #423 - Relates to #426 - Relates to #427 --- build.gradle | 1 + settings.gradle | 1 + .../config/AbstractStateMachineFactory.java | 13 ++ .../StateMachineConfigurationBuilder.java | 31 +++- .../StateMachineConfigurationConfigurer.java | 9 ++ .../DefaultPersistenceConfigurer.java | 48 ++++++ .../configurers/PersistenceConfigurer.java | 40 +++++ .../config/model/ConfigurationData.java | 20 ++- ...ractPersistingStateMachineInterceptor.java | 153 ++++++++++++++++++ .../persist/StateMachineRuntimePersister.java | 37 +++++ .../config/model/StateMachineModelTests.java | 2 +- .../JpaPersistingStateMachineInterceptor.java | 72 +++++++++ .../data/jpa/JpaRepositoryStateMachine.java | 72 +++++++++ .../jpa/JpaRepositoryStateMachinePersist.java | 57 +++++++ .../data/jpa/JpaStateMachineRepository.java | 28 ++++ .../data/jpa/JpaRepositoryTests.java | 145 ++++++++++++++++- .../data/RepositoryStateMachine.java | 46 ++++++ .../RepositoryStateMachineModelFactory.java | 8 +- .../data/RepositoryStateMachinePersist.java | 71 ++++++++ .../data/StateMachineRepository.java | 30 ++++ .../kryo/KryoSerialisationService.java | 87 ++++++++++ spring-statemachine-samples/build.gradle | 12 ++ .../java/demo/datajpapersist/Application.java | 29 ++++ .../datajpapersist/StateMachineConfig.java | 105 ++++++++++++ .../StateMachineController.java | 128 +++++++++++++++ .../StateMachineLogListener.java | 52 ++++++ .../src/main/resources/application.yml | 6 + .../src/main/resources/templates/states.html | 45 ++++++ .../datajpapersist/DataJpaPersistTests.java | 107 ++++++++++++ 29 files changed, 1442 insertions(+), 13 deletions(-) create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultPersistenceConfigurer.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/PersistenceConfigurer.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java create mode 100644 spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachineRuntimePersister.java create mode 100644 spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaPersistingStateMachineInterceptor.java create mode 100644 spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachine.java create mode 100644 spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java create mode 100644 spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaStateMachineRepository.java create mode 100644 spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachine.java create mode 100644 spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java create mode 100644 spring-statemachine-data/src/main/java/org/springframework/statemachine/data/StateMachineRepository.java create mode 100644 spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoSerialisationService.java create mode 100644 spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/Application.java create mode 100644 spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineConfig.java create mode 100644 spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineController.java create mode 100644 spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineLogListener.java create mode 100644 spring-statemachine-samples/datajpapersist/src/main/resources/application.yml create mode 100644 spring-statemachine-samples/datajpapersist/src/main/resources/templates/states.html create mode 100644 spring-statemachine-samples/datajpapersist/src/test/java/demo/datajpapersist/DataJpaPersistTests.java diff --git a/build.gradle b/build.gradle index 2f6406ea..4966f4de 100644 --- a/build.gradle +++ b/build.gradle @@ -234,6 +234,7 @@ project('spring-statemachine-data-common') { } dependencies { compile project(":spring-statemachine-core") + compile project(":spring-statemachine-kryo") compile "org.springframework.data:spring-data-commons:$springDataCommonsVersion" optional "org.springframework.security:spring-security-core:$springSecurityVersion" compile "com.fasterxml.jackson.core:jackson-core:$jackson2Version" diff --git a/settings.gradle b/settings.gradle index 2ac87c9a..e9f4aba0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,6 +26,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:datajpapersist' include 'spring-statemachine-samples:monitoring' include 'spring-statemachine-data' 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 8d4907f8..46402291 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 @@ -79,6 +79,7 @@ import org.springframework.statemachine.state.StateHolder; 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.tree.Tree; import org.springframework.statemachine.support.tree.Tree.Node; import org.springframework.statemachine.support.tree.TreeTraverser; @@ -319,6 +320,18 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS machine.addStateListener(listener); } + List> interceptors = stateMachineModel.getConfigurationData().getStateMachineInterceptors(); + if (interceptors != null) { + for (final StateMachineInterceptor interceptor : interceptors) { + machine.getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { + @Override + public void apply(StateMachineAccess function) { + function.addStateMachineInterceptor(interceptor); + } + }); + } + } + // go through holders and fix state references which // were not known at a time holder was created for (HolderListItem holderItem : holderList) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java index 85862314..e1c4aa37 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-2017 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. @@ -29,10 +29,12 @@ import org.springframework.statemachine.config.configurers.ConfigurationConfigur import org.springframework.statemachine.config.configurers.DefaultConfigurationConfigurer; import org.springframework.statemachine.config.configurers.DefaultDistributedStateMachineConfigurer; import org.springframework.statemachine.config.configurers.DefaultMonitoringConfigurer; +import org.springframework.statemachine.config.configurers.DefaultPersistenceConfigurer; import org.springframework.statemachine.config.configurers.DefaultSecurityConfigurer; import org.springframework.statemachine.config.configurers.DefaultVerifierConfigurer; import org.springframework.statemachine.config.configurers.DistributedStateMachineConfigurer; import org.springframework.statemachine.config.configurers.MonitoringConfigurer; +import org.springframework.statemachine.config.configurers.PersistenceConfigurer; import org.springframework.statemachine.config.configurers.SecurityConfigurer; import org.springframework.statemachine.config.configurers.VerifierConfigurer; import org.springframework.statemachine.config.model.ConfigurationData; @@ -41,7 +43,9 @@ import org.springframework.statemachine.config.model.verifier.StateMachineModelV import org.springframework.statemachine.ensemble.StateMachineEnsemble; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.monitor.StateMachineMonitor; +import org.springframework.statemachine.persist.StateMachineRuntimePersister; import org.springframework.statemachine.security.SecurityRule; +import org.springframework.statemachine.support.StateMachineInterceptor; /** * {@link AnnotationBuilder} for {@link StatesData}. @@ -70,6 +74,8 @@ public class StateMachineConfigurationBuilder private SecurityRule eventSecurityRule; private SecurityRule transitionSecurityRule; private StateMachineMonitor stateMachineMonitor; + private final List> interceptors = new ArrayList>(); + private StateMachineRuntimePersister persister; /** * Instantiates a new state machine configuration builder. @@ -123,11 +129,23 @@ public class StateMachineConfigurationBuilder return apply(new DefaultMonitoringConfigurer()); } + @Override + public PersistenceConfigurer withPersistence() throws Exception { + return apply(new DefaultPersistenceConfigurer()); + } + @Override protected ConfigurationData performBuild() throws Exception { + ArrayList> interceptorsCopy = new ArrayList>(interceptors); + if (persister != null) { + StateMachineInterceptor interceptor = persister.getInterceptor(); + if (interceptor != null) { + interceptorsCopy.add((StateMachineInterceptor) interceptor); + } + } return new ConfigurationData(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners, securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule, - transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor); + transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor, interceptorsCopy); } /** @@ -265,4 +283,13 @@ public class StateMachineConfigurationBuilder public void setVerifier(StateMachineModelVerifier verifier) { this.verifier = verifier; } + + /** + * Sets the state machine runtime persister. + * + * @param persister the state machine runtime persister + */ + public void setStateMachineRuntimePersister(StateMachineRuntimePersister persister) { + this.persister = persister; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java index 33213e29..159f0daf 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationConfigurer.java @@ -18,6 +18,7 @@ package org.springframework.statemachine.config.builders; import org.springframework.statemachine.config.configurers.ConfigurationConfigurer; import org.springframework.statemachine.config.configurers.DistributedStateMachineConfigurer; import org.springframework.statemachine.config.configurers.MonitoringConfigurer; +import org.springframework.statemachine.config.configurers.PersistenceConfigurer; import org.springframework.statemachine.config.configurers.SecurityConfigurer; import org.springframework.statemachine.config.configurers.VerifierConfigurer; @@ -70,4 +71,12 @@ public interface StateMachineConfigurationConfigurer { * @throws Exception if configuration error happens */ MonitoringConfigurer withMonitoring() throws Exception; + + /** + * Gets a configurer for state machine persistence. + * + * @return {@link PersistenceConfigurer} for chaining + * @throws Exception if configuration error happens + */ + PersistenceConfigurer withPersistence() throws Exception; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultPersistenceConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultPersistenceConfigurer.java new file mode 100644 index 00000000..25b96ef2 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultPersistenceConfigurer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 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.config.configurers; + +import org.springframework.statemachine.config.builders.StateMachineConfigurationBuilder; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter; +import org.springframework.statemachine.config.model.ConfigurationData; +import org.springframework.statemachine.persist.StateMachineRuntimePersister; + +/** + * Default implementation of a {@link PersistenceConfigurer}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class DefaultPersistenceConfigurer extends + AnnotationConfigurerAdapter, StateMachineConfigurationConfigurer, StateMachineConfigurationBuilder> + implements PersistenceConfigurer { + + private StateMachineRuntimePersister persister; + + @Override + public void configure(StateMachineConfigurationBuilder builder) throws Exception { + builder.setStateMachineRuntimePersister(persister); + } + + @Override + public PersistenceConfigurer runtimePersister(StateMachineRuntimePersister persister) { + this.persister = persister; + return this; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/PersistenceConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/PersistenceConfigurer.java new file mode 100644 index 00000000..5150de6a --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/PersistenceConfigurer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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.config.configurers; + +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder; +import org.springframework.statemachine.persist.StateMachineRuntimePersister; + +/** + * Base {@code PersistenceConfigurer} interface for configuring state machine persistence. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface PersistenceConfigurer extends + AnnotationConfigurerBuilder> { + + /** + * Specify a state machine runtime persister. + * + * @param persister the state machine runtime persister + * @return configurer for chaining + */ + PersistenceConfigurer runtimePersister(StateMachineRuntimePersister persister); +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java index c46eb540..9ac1a5de 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2017 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. @@ -31,6 +31,7 @@ import org.springframework.statemachine.ensemble.StateMachineEnsemble; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.monitor.StateMachineMonitor; import org.springframework.statemachine.security.SecurityRule; +import org.springframework.statemachine.support.StateMachineInterceptor; /** * Configuration object used to keep things together in {@link StateMachineConfigurationBuilder}. @@ -57,13 +58,14 @@ public class ConfigurationData { private final SecurityRule eventSecurityRule; private final SecurityRule transitionSecurityRule; private final StateMachineMonitor stateMachineMonitor; + private final List> interceptors; /** * Instantiates a new state machine configuration config data. */ public ConfigurationData() { this(null, new SyncTaskExecutor(), new ConcurrentTaskScheduler(), false, null, new ArrayList>(), false, - null, null, null, null, true, new DefaultStateMachineModelVerifier(), null, null); + null, null, null, null, true, new DefaultStateMachineModelVerifier(), null, null, null); } /** @@ -84,13 +86,15 @@ public class ConfigurationData { * @param verifier the state machine model verifier * @param machineId the machine id * @param stateMachineMonitor the state machine monitor + * @param interceptors the state machine interceptors. */ public ConfigurationData(BeanFactory beanFactory, TaskExecutor taskExecutor, TaskScheduler taskScheduler, boolean autoStart, StateMachineEnsemble ensemble, List> listeners, boolean securityEnabled, AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager, SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled, - StateMachineModelVerifier verifier, String machineId, StateMachineMonitor stateMachineMonitor) { + StateMachineModelVerifier verifier, String machineId, StateMachineMonitor stateMachineMonitor, + List> interceptors) { this.beanFactory = beanFactory; this.taskExecutor = taskExecutor; this.taskScheduler = taskScheduler; @@ -106,6 +110,7 @@ public class ConfigurationData { this.verifier = verifier; this.machineId = machineId; this.stateMachineMonitor = stateMachineMonitor; + this.interceptors = interceptors; } public String getMachineId() { @@ -237,4 +242,13 @@ public class ConfigurationData { public SecurityRule getTransitionSecurityRule() { return transitionSecurityRule; } + + /** + * Gets the state machine interceptors. + * + * @return the state machine interceptors + */ + public List> getStateMachineInterceptors() { + return interceptors; + } } 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 new file mode 100644 index 00000000..4255e675 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractPersistingStateMachineInterceptor.java @@ -0,0 +1,153 @@ +/* + * Copyright 2017 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.persist; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.messaging.Message; +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.StateMachineException; +import org.springframework.statemachine.StateMachinePersist; +import org.springframework.statemachine.region.Region; +import org.springframework.statemachine.state.AbstractState; +import org.springframework.statemachine.state.HistoryPseudoState; +import org.springframework.statemachine.state.PseudoState; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.support.AbstractStateMachine; +import org.springframework.statemachine.support.DefaultExtendedState; +import org.springframework.statemachine.support.DefaultStateMachineContext; +import org.springframework.statemachine.support.StateMachineInterceptor; +import org.springframework.statemachine.support.StateMachineInterceptorAdapter; +import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.transition.TransitionKind; + +/** + * Base class for {@link StateMachineInterceptor} persisting {@link StateMachineContext}s. + * This class is to be used as a base implementation which wants to persist a machine which + * is about to kept running as normal use case for persistence is to stop machine, persist and + * then start it again. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public abstract class AbstractPersistingStateMachineInterceptor extends StateMachineInterceptorAdapter + implements StateMachinePersist { + + @Override + public void preStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { + // 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), null); + } catch (Exception e) { + throw new StateMachineException("Unable to persist stateMachineContext", e); + } + } + + @Override + public void postStateChange(State state, Message message, Transition transition, StateMachine stateMachine) { + // 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), null); + } catch (Exception e) { + throw new StateMachineException("Unable to persist stateMachineContext", e); + } + } + } + + /** + * Write {@link StateMachineContext} into persistent store. + * + * @param context the state machine context + * @param contextObj the context object + */ + public abstract void write(StateMachineContext context, Object contextObj) throws Exception; + + /** + * Read {@link StateMachineContext} from persistent store. + * + * @param contextObj the context object + * @return the state machine context + */ + public abstract StateMachineContext read(Object contextObj) throws Exception; + + /** + * Builds the state machine context. + * + * @param stateMachine the state machine + * @param state the state + * @return the state machine context + */ + protected StateMachineContext buildStateMachineContext(StateMachine stateMachine, State state) { + ExtendedState extendedState = new DefaultExtendedState(); + extendedState.getVariables().putAll(stateMachine.getExtendedState().getVariables()); + + ArrayList> childs = 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)); + } + id = state.getId(); + } else { + id = state.getId(); + } + + // building history state mappings + Map historyStates = new HashMap(); + PseudoState historyState = ((AbstractStateMachine) stateMachine).getHistoryState(); + if (historyState != null) { + historyStates.put(null, ((HistoryPseudoState)historyState).getState().getId()); + } + Collection> states = stateMachine.getStates(); + for (State ss : states) { + if (ss.isSubmachineState()) { + StateMachine submachine = ((AbstractState) ss).getSubmachine(); + PseudoState ps = ((AbstractStateMachine) submachine).getHistoryState(); + if (ps != null) { + State pss = ((HistoryPseudoState)ps).getState(); + if (pss != null) { + historyStates.put(ss.getId(), pss.getId()); + } + } + } + } + return new DefaultStateMachineContext(childs, id, null, null, extendedState, historyStates, stateMachine.getId()); + } + + private S getDeepState(State state) { + Collection ids1 = state.getIds(); + @SuppressWarnings("unchecked") + S[] ids2 = (S[]) ids1.toArray(); + // TODO: can this be empty as then we'd get error? + return ids2[ids2.length-1]; + } +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachineRuntimePersister.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachineRuntimePersister.java new file mode 100644 index 00000000..edb725cc --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachineRuntimePersister.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 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.persist; + +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.support.StateMachineInterceptor; + +/** + * Interface defining a runtime persistence of a {@link StateMachine}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface StateMachineRuntimePersister { + + /** + * Gets a {@link StateMachineInterceptor} handling machine persistence. + * + * @return the interceptor handling persistence + */ + StateMachineInterceptor getInterceptor(); +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java index 85edce0e..70d8787f 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java @@ -56,7 +56,7 @@ public class StateMachineModelTests { ConfigurationData configurationData = new ConfigurationData<>(beanFactory, taskExecutor, taskScheduler, autoStart, ensemble, listeners, securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, - eventSecurityRule, transitionSecurityRule, verifierEnabled, verifier, null, null); + eventSecurityRule, transitionSecurityRule, verifierEnabled, verifier, null, null, null); Collection> stateData = new ArrayList<>(); StateData stateData1 = new StateData(null, null, "S1", null, null, null); diff --git a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaPersistingStateMachineInterceptor.java b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaPersistingStateMachineInterceptor.java new file mode 100644 index 00000000..26e661ec --- /dev/null +++ b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaPersistingStateMachineInterceptor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017 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.data.jpa; + +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.StateMachinePersist; +import org.springframework.statemachine.persist.AbstractPersistingStateMachineInterceptor; +import org.springframework.statemachine.persist.StateMachineRuntimePersister; +import org.springframework.statemachine.support.StateMachineInterceptor; +import org.springframework.util.Assert; + +/** + * {@code JPA} implementation of a {@link AbstractPersistingStateMachineInterceptor}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class JpaPersistingStateMachineInterceptor extends AbstractPersistingStateMachineInterceptor + implements StateMachinePersist, StateMachineRuntimePersister { + + private final JpaRepositoryStateMachinePersist persist; + + /** + * Instantiates a new jpa persisting state machine interceptor. + * + * @param jpaStateMachineRepository the jpa state machine repository + */ + public JpaPersistingStateMachineInterceptor(JpaStateMachineRepository jpaStateMachineRepository) { + Assert.notNull(jpaStateMachineRepository, "'jpaStateMachineRepository' must be set"); + this.persist = new JpaRepositoryStateMachinePersist(jpaStateMachineRepository); + } + + /** + * Instantiates a new jpa persisting state machine interceptor. + * + * @param persist the persist + */ + public JpaPersistingStateMachineInterceptor(JpaRepositoryStateMachinePersist persist) { + Assert.notNull(persist, "'persist' must be set"); + this.persist = persist; + } + + @Override + public StateMachineInterceptor getInterceptor() { + return this; + } + + @Override + public void write(StateMachineContext context, Object contextObj) throws Exception { + persist.write(context, contextObj); + } + + @Override + public StateMachineContext read(Object contextObj) throws Exception { + return persist.read(contextObj); + } +} diff --git a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachine.java b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachine.java new file mode 100644 index 00000000..1bc93cbe --- /dev/null +++ b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachine.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017 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.data.jpa; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; + +import org.springframework.statemachine.data.RepositoryStateMachine; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; + +/** + * A {@link RepositoryStateMachine} interface for JPA used for states machines. + * + * @author Janne Valkealahti + * + */ +@Entity +@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class) +public class JpaRepositoryStateMachine extends RepositoryStateMachine { + + @Id + private String machineId; + + private String state; + + @Lob + private byte[] stateMachineContext; + + @Override + public String getMachineId() { + return machineId; + } + + public void setMachineId(String machineId) { + this.machineId = machineId; + } + + @Override + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + @Override + public byte[] getStateMachineContext() { + return stateMachineContext; + } + + public void setStateMachineContext(byte[] stateMachineContext) { + this.stateMachineContext = stateMachineContext; + } + +} 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 new file mode 100644 index 00000000..a4e7271d --- /dev/null +++ b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaRepositoryStateMachinePersist.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 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.data.jpa; + +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.data.RepositoryStateMachinePersist; +import org.springframework.statemachine.data.StateMachineRepository; + +/** + * {@code JPA} based implementation of a {@link RepositoryStateMachinePersist}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class JpaRepositoryStateMachinePersist extends RepositoryStateMachinePersist { + + private final JpaStateMachineRepository jpaStateMachineRepository; + + /** + * Instantiates a new jpa repository state machine persist. + * + * @param jpaStateMachineRepository the jpa state machine repository + */ + public JpaRepositoryStateMachinePersist(JpaStateMachineRepository jpaStateMachineRepository) { + super(); + this.jpaStateMachineRepository = jpaStateMachineRepository; + } + + @Override + protected StateMachineRepository getRepository() { + return jpaStateMachineRepository; + } + + @Override + protected JpaRepositoryStateMachine build(StateMachineContext context, byte[] serialisedContext) { + JpaRepositoryStateMachine jpaRepositoryStateMachine = new JpaRepositoryStateMachine(); + jpaRepositoryStateMachine.setMachineId(context.getId()); + jpaRepositoryStateMachine.setState(context.getState().toString()); + jpaRepositoryStateMachine.setStateMachineContext(serialisedContext); + return jpaRepositoryStateMachine; + } +} diff --git a/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaStateMachineRepository.java b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaStateMachineRepository.java new file mode 100644 index 00000000..b8829601 --- /dev/null +++ b/spring-statemachine-data/jpa/src/main/java/org/springframework/statemachine/data/jpa/JpaStateMachineRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017 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.data.jpa; + +import org.springframework.statemachine.data.StateMachineRepository; + +/** + * JPA entity for state machine. + * A {@link StateMachineRepository} interface for JPA used for states machines. + * + * @author Janne Valkealahti + * + */ +public interface JpaStateMachineRepository extends StateMachineRepository{ +} 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 8500f987..5b64f0af 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 the original author or authors. + * Copyright 2016-2017 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. @@ -27,12 +27,21 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.data.AbstractRepositoryTests; import org.springframework.statemachine.data.RepositoryState; import org.springframework.statemachine.data.RepositoryTransition; +import org.springframework.statemachine.data.StateMachineRepository; import org.springframework.statemachine.data.StateRepository; import org.springframework.statemachine.data.TransitionRepository; +import org.springframework.statemachine.persist.StateMachineRuntimePersister; import org.springframework.statemachine.transition.TransitionKind; /** @@ -253,6 +262,36 @@ public class JpaRepositoryTests extends AbstractRepositoryTests { context.refresh(); } + @Test + @SuppressWarnings("unchecked") + public void testStateMachinePersistWithStrings() { + context.register(TestConfig.class, ConfigWithStrings.class); + context.refresh(); + + StateMachine stateMachine = context.getBean(StateMachine.class); + stateMachine.start(); + assertThat(stateMachine.getState().getId(), is("S1")); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getId(), is("S2")); + stateMachine.sendEvent("E2"); + assertThat(stateMachine.getState().getId(), is("S1")); + } + + @Test + @SuppressWarnings("unchecked") + public void testStateMachinePersistWithEnums() { + context.register(TestConfig.class, ConfigWithEnums.class); + context.refresh(); + + StateMachine stateMachine = context.getBean(StateMachine.class); + stateMachine.start(); + assertThat(stateMachine.getState().getId(), is(PersistTestStates.S1)); + stateMachine.sendEvent(PersistTestEvents.E1); + assertThat(stateMachine.getState().getId(), is(PersistTestStates.S2)); + stateMachine.sendEvent(PersistTestEvents.E2); + assertThat(stateMachine.getState().getId(), is(PersistTestStates.S1)); + } + @EnableAutoConfiguration static class TestConfig { } @@ -275,6 +314,110 @@ public class JpaRepositoryTests extends AbstractRepositoryTests { @Autowired StateRepository statesRepository4; + + @Autowired + JpaStateMachineRepository jpaStateMachineRepository1; + + @Autowired + StateMachineRepository jpaStateMachineRepository2; + } + @Configuration + @EnableStateMachine + public static class ConfigWithStrings extends StateMachineConfigurerAdapter { + + @Autowired + private JpaStateMachineRepository jpaStateMachineRepository; + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .machineId("xxx1") + .and() + .withPersistence() + .runtimePersister(stateMachineRuntimePersister()); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S1") + .event("E2"); + } + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister() { + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } + } + + @Configuration + @EnableStateMachine + public static class ConfigWithEnums extends StateMachineConfigurerAdapter { + + @Autowired + private JpaStateMachineRepository jpaStateMachineRepository; + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .machineId("xxx2") + .and() + .withPersistence() + .runtimePersister(stateMachineRuntimePersister()); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(PersistTestStates.S1) + .state(PersistTestStates.S2); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(PersistTestStates.S1) + .target(PersistTestStates.S2) + .event(PersistTestEvents.E1) + .and() + .withExternal() + .source(PersistTestStates.S2) + .target(PersistTestStates.S1) + .event(PersistTestEvents.E2); + } + + @Bean + public StateMachineRuntimePersister stateMachineRuntimePersister() { + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } + } + + public enum PersistTestStates { + S1, S2; + } + + public enum PersistTestEvents { + E1, E2; + } } diff --git a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachine.java b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachine.java new file mode 100644 index 00000000..9eb50dbe --- /dev/null +++ b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachine.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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.data; + +/** + * Generic base class representing state machine entity. + * + * @author Janne Valkealahti + * + */ +public abstract class RepositoryStateMachine extends BaseRepositoryEntity { + + /** + * Gets the machine id. + * + * @return the machine id + */ + public abstract String getMachineId(); + + /** + * Gets the state. + * + * @return the state + */ + public abstract String getState(); + + /** + * Gets the state machine context. + * + * @return the state machine context + */ + public abstract byte[] getStateMachineContext(); +} diff --git a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachineModelFactory.java b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachineModelFactory.java index 6d90ba5a..e27e2ae1 100644 --- a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachineModelFactory.java +++ b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachineModelFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -30,7 +30,6 @@ import org.springframework.statemachine.action.Action; import org.springframework.statemachine.action.SpelExpressionAction; import org.springframework.statemachine.config.model.AbstractStateMachineModelFactory; import org.springframework.statemachine.config.model.ChoiceData; -import org.springframework.statemachine.config.model.ConfigurationData; import org.springframework.statemachine.config.model.DefaultStateMachineModel; import org.springframework.statemachine.config.model.EntryData; import org.springframework.statemachine.config.model.ExitData; @@ -80,8 +79,6 @@ public class RepositoryStateMachineModelFactory extends AbstractStateMachineMode @Override public StateMachineModel build(String machineId) { - ConfigurationData configurationData = new ConfigurationData<>(); - Collection> stateDatas = new ArrayList<>(); for (RepositoryState s : stateRepository.findByMachineId(machineId == null ? "" : machineId)) { @@ -284,8 +281,7 @@ public class RepositoryStateMachineModelFactory extends AbstractStateMachineMode TransitionsData transitionsData = new TransitionsData<>(transitionData, choicesCopy, junctionsCopy, forks, joins, entrys, exits, historys); - StateMachineModel stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData, transitionsData); - return stateMachineModel; + return new DefaultStateMachineModel<>(null, statesData, transitionsData); } private Guard resolveGuard(RepositoryTransition t) { 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 new file mode 100644 index 00000000..fb228ad5 --- /dev/null +++ b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/RepositoryStateMachinePersist.java @@ -0,0 +1,71 @@ +/* + * Copyright 2017 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.data; + +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.KryoSerialisationService; + +/** + * Base implementation of a {@link StateMachinePersist} using Spring Data Repositories. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + * @param the type of entity + */ +public abstract class RepositoryStateMachinePersist implements StateMachinePersist { + + private final Log log = LogFactory.getLog(RepositoryStateMachinePersist.class); + private final KryoSerialisationService serialisationService = new KryoSerialisationService(); + + @Override + public void write(StateMachineContext context, Object contextObj) throws Exception { + if (log.isDebugEnabled()) { + log.debug("Persisting context " + context + " using contextObj " + contextObj); + } + M build = build(context, serialisationService.serializeStateMachineContext(context)); + getRepository().save(build); + } + + @Override + public StateMachineContext read(Object contextObj) throws Exception { + M repositoryStateMachine = getRepository().findOne(contextObj.toString()); + if (repositoryStateMachine != null) { + return serialisationService.deserializeStateMachineContext(repositoryStateMachine.getStateMachineContext()); + } + return null; + } + + /** + * Gets the repository. + * + * @return the repository + */ + protected abstract StateMachineRepository getRepository(); + + /** + * Builds the generic {@link RepositoryStateMachine} entity. + * + * @param context the context + * @param serialisedContext the serialised context + * @return the repository state machine entity + */ + protected abstract M build(StateMachineContext context, byte[] serialisedContext); +} diff --git a/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/StateMachineRepository.java b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/StateMachineRepository.java new file mode 100644 index 00000000..28fe733c --- /dev/null +++ b/spring-statemachine-data/src/main/java/org/springframework/statemachine/data/StateMachineRepository.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017 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.data; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.Repository; + +/** + * Generic {@link Repository} interface for states machines. + * + * @author Janne Valkealahti + * + */ +@NoRepositoryBean +public interface StateMachineRepository extends CrudRepository { +} diff --git a/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoSerialisationService.java b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoSerialisationService.java new file mode 100644 index 00000000..80cc29ab --- /dev/null +++ b/spring-statemachine-kryo/src/main/java/org/springframework/statemachine/kryo/KryoSerialisationService.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017 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.UUID; + +import org.springframework.messaging.MessageHeaders; +import org.springframework.statemachine.StateMachineContext; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Simple service and utility class helping with kryo serialisation. + * + * @author Janne Valkealahti + * + */ +public class KryoSerialisationService { + + // kryo is not a thread safe so using thread local, also + // adding custom serializer for state machine context. + private static final ThreadLocal kryoThreadLocal = new ThreadLocal() { + + @SuppressWarnings("rawtypes") + @Override + protected Kryo initialValue() { + Kryo kryo = new Kryo(); + kryo.addDefaultSerializer(StateMachineContext.class, new StateMachineContextSerializer()); + kryo.addDefaultSerializer(MessageHeaders.class, new MessageHeadersSerializer()); + kryo.addDefaultSerializer(UUID.class, new UUIDSerializer()); + return kryo; + } + }; + + /** + * Serialize state machine context into byte array. + * + * @param the generic type + * @param the element type + * @param context the context + * @return the byte[] + */ + public byte[] serializeStateMachineContext(StateMachineContext context) { + Kryo kryo = kryoThreadLocal.get(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Output output = new Output(out); + kryo.writeObject(output, context); + output.close(); + return out.toByteArray(); + } + + /** + * Deserialize state machine context from byte array. + * + * @param the generic type + * @param the element type + * @param data the data + * @return the state machine context + */ + @SuppressWarnings("unchecked") + public StateMachineContext deserializeStateMachineContext(byte[] data) { + if (data == null || data.length == 0) { + return null; + } + Kryo kryo = kryoThreadLocal.get(); + ByteArrayInputStream in = new ByteArrayInputStream(data); + Input input = new Input(in); + return kryo.readObject(input, StateMachineContext.class); + } +} diff --git a/spring-statemachine-samples/build.gradle b/spring-statemachine-samples/build.gradle index 766faf2d..5879b84d 100644 --- a/spring-statemachine-samples/build.gradle +++ b/spring-statemachine-samples/build.gradle @@ -121,6 +121,18 @@ project('spring-statemachine-samples-datajpa') { } } +project('spring-statemachine-samples-datajpapersist') { + description = 'Spring State Machine Data Jpa Persist Sample' + dependencies { + compile project(":spring-statemachine-boot") + compile project(":spring-statemachine-data-common:spring-statemachine-data-jpa") + compile("org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion") + compile("org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion") + compile("org.springframework.boot:spring-boot-devtools:$springBootVersion") + compile("com.h2database:h2:$h2Version") + } +} + project('spring-statemachine-samples-monitoring') { description = 'Spring State Machine Monitoring Sample' dependencies { diff --git a/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/Application.java b/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/Application.java new file mode 100644 index 00000000..2f851de3 --- /dev/null +++ b/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/Application.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 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.datajpapersist; + +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/datajpapersist/src/main/java/demo/datajpapersist/StateMachineConfig.java b/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineConfig.java new file mode 100644 index 00000000..490408a5 --- /dev/null +++ b/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineConfig.java @@ -0,0 +1,105 @@ +/* + * Copyright 2017 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.datajpapersist; + +import java.util.EnumSet; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.config.EnableStateMachineFactory; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.data.jpa.JpaPersistingStateMachineInterceptor; +import org.springframework.statemachine.data.jpa.JpaStateMachineRepository; +import org.springframework.statemachine.persist.StateMachineRuntimePersister; + +@Configuration +public class StateMachineConfig { + +//tag::snippetA[] + @Configuration + @EnableStateMachineFactory + public static class Config extends StateMachineConfigurerAdapter { + + @Autowired + private JpaStateMachineRepository jpaStateMachineRepository; + + @Override + public void configure(StateMachineConfigurationConfigurer config) + throws Exception { + config + .withPersistence() + .runtimePersister(stateMachineruntimePersister()); + } + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial(States.S1) + .states(EnumSet.allOf(States.class)); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source(States.S1).target(States.S2) + .event(Events.E1) + .and() + .withExternal() + .source(States.S2).target(States.S3) + .event(Events.E2) + .and() + .withExternal() + .source(States.S3).target(States.S4) + .event(Events.E3) + .and() + .withExternal() + .source(States.S4).target(States.S5) + .event(Events.E4) + .and() + .withExternal() + .source(States.S5).target(States.S6) + .event(Events.E5) + .and() + .withExternal() + .source(States.S6).target(States.S1) + .event(Events.E6); + } + + @Bean + public StateMachineRuntimePersister stateMachineruntimePersister() { + return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); + } + } +//end::snippetA[] + +//tag::snippetB[] + public enum States { + S1, S2, S3, S4, S5, S6; + } + + public enum Events { + E1, E2, E3, E4, E5, E6; + } +//end::snippetB[] +} diff --git a/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineController.java b/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineController.java new file mode 100644 index 00000000..9ed291c1 --- /dev/null +++ b/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineController.java @@ -0,0 +1,128 @@ +/* + * Copyright 2017 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.datajpapersist; + +import java.util.EnumSet; +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.access.StateMachineAccess; +import org.springframework.statemachine.access.StateMachineFunction; +import org.springframework.statemachine.config.StateMachineFactory; +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; + +import demo.datajpapersist.StateMachineConfig.Events; +import demo.datajpapersist.StateMachineConfig.States; + +@Controller +public class StateMachineController { + + public final static String MACHINE_ID_1 = "datajpapersist1"; + public final static String MACHINE_ID_2 = "datajpapersist2"; + private final static String[] MACHINES = new String[] { MACHINE_ID_1, MACHINE_ID_2 }; + + @Autowired + private StateMachineFactory stateMachineFactory; + + @Autowired + private StateMachinePersist stateMachinePersist; + + private StateMachine cachedStateMachine; + private final StateMachineLogListener listener = new StateMachineLogListener(); + + @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 (Events event : events) { + stateMachine.sendEvent(event); + } + } + StateMachineContext stateMachineContext = stateMachinePersist.read(machine); + model.addAttribute("allMachines", MACHINES); + model.addAttribute("machine", machine); + model.addAttribute("allEvents", getEvents()); + model.addAttribute("messages", createMessages(listener.getMessages())); + model.addAttribute("context", stateMachineContext != null ? stateMachineContext.toString() : ""); + return "states"; + } + + private synchronized StateMachine getStateMachine(String machineId) throws Exception { + if (cachedStateMachine == null) { + cachedStateMachine = buildStateMachine(machineId); + cachedStateMachine.start(); + } else { + if (!ObjectUtils.nullSafeEquals(cachedStateMachine.getId(), machineId)) { + cachedStateMachine.stop(); + cachedStateMachine = buildStateMachine(machineId); + cachedStateMachine.start(); + } + } + return cachedStateMachine; + } + + private StateMachine buildStateMachine(String machineId) throws Exception { + StateMachine stateMachine = stateMachineFactory.getStateMachine(machineId); + stateMachine.addStateListener(listener); + listener.resetMessages(); + return restoreStateMachine(stateMachine, stateMachinePersist.read(machineId)); + } + + private StateMachine restoreStateMachine(StateMachine stateMachine, + StateMachineContext stateMachineContext) { + if (stateMachineContext == null) { + return stateMachine; + } + stateMachine.stop(); + stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction>() { + + @Override + public void apply(StateMachineAccess function) { + function.resetStateMachine(stateMachineContext); + } + }); + return stateMachine; + } + + private Events[] getEvents() { + return EnumSet.allOf(Events.class).toArray(new Events[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/datajpapersist/src/main/java/demo/datajpapersist/StateMachineLogListener.java b/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineLogListener.java new file mode 100644 index 00000000..5bdf7e76 --- /dev/null +++ b/spring-statemachine-samples/datajpapersist/src/main/java/demo/datajpapersist/StateMachineLogListener.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 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.datajpapersist; + +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; + +import demo.datajpapersist.StateMachineConfig.Events; +import demo.datajpapersist.StateMachineConfig.States; + +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/datajpapersist/src/main/resources/application.yml b/spring-statemachine-samples/datajpapersist/src/main/resources/application.yml new file mode 100644 index 00000000..d560ab3a --- /dev/null +++ b/spring-statemachine-samples/datajpapersist/src/main/resources/application.yml @@ -0,0 +1,6 @@ +logging: + level: + root: INFO +security: + basic: + enabled: false diff --git a/spring-statemachine-samples/datajpapersist/src/main/resources/templates/states.html b/spring-statemachine-samples/datajpapersist/src/main/resources/templates/states.html new file mode 100644 index 00000000..21dba05a --- /dev/null +++ b/spring-statemachine-samples/datajpapersist/src/main/resources/templates/states.html @@ -0,0 +1,45 @@ + + + + Spring Statemachine Demo + + + + +
+
+

+

    +
  • + + +
  • +
+
+
+

+

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

Events

+
+
+