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
This commit is contained in:
Janne Valkealahti
2017-11-18 16:38:49 +00:00
parent d82bca1280
commit 33a5370d01
29 changed files with 1442 additions and 13 deletions

View File

@@ -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"

View File

@@ -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'

View File

@@ -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<S, E> extends LifecycleObjectS
machine.addStateListener(listener);
}
List<StateMachineInterceptor<S,E>> interceptors = stateMachineModel.getConfigurationData().getStateMachineInterceptors();
if (interceptors != null) {
for (final StateMachineInterceptor<S, E> interceptor : interceptors) {
machine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S,E>>() {
@Override
public void apply(StateMachineAccess<S, E> function) {
function.addStateMachineInterceptor(interceptor);
}
});
}
}
// go through holders and fix state references which
// were not known at a time holder was created
for (HolderListItem<S, E> holderItem : holderList) {

View File

@@ -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<S, E>
private SecurityRule eventSecurityRule;
private SecurityRule transitionSecurityRule;
private StateMachineMonitor<S, E> stateMachineMonitor;
private final List<StateMachineInterceptor<S, E>> interceptors = new ArrayList<StateMachineInterceptor<S, E>>();
private StateMachineRuntimePersister<S, E> persister;
/**
* Instantiates a new state machine configuration builder.
@@ -123,11 +129,23 @@ public class StateMachineConfigurationBuilder<S, E>
return apply(new DefaultMonitoringConfigurer<S, E>());
}
@Override
public PersistenceConfigurer<S, E> withPersistence() throws Exception {
return apply(new DefaultPersistenceConfigurer<S, E>());
}
@Override
protected ConfigurationData<S, E> performBuild() throws Exception {
ArrayList<StateMachineInterceptor<S, E>> interceptorsCopy = new ArrayList<StateMachineInterceptor<S, E>>(interceptors);
if (persister != null) {
StateMachineInterceptor<S, E> interceptor = persister.getInterceptor();
if (interceptor != null) {
interceptorsCopy.add((StateMachineInterceptor<S, E>) interceptor);
}
}
return new ConfigurationData<S, E>(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<S, E>
public void setVerifier(StateMachineModelVerifier<S, E> verifier) {
this.verifier = verifier;
}
/**
* Sets the state machine runtime persister.
*
* @param persister the state machine runtime persister
*/
public void setStateMachineRuntimePersister(StateMachineRuntimePersister<S, E> persister) {
this.persister = persister;
}
}

View File

@@ -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<S, E> {
* @throws Exception if configuration error happens
*/
MonitoringConfigurer<S, E> withMonitoring() throws Exception;
/**
* Gets a configurer for state machine persistence.
*
* @return {@link PersistenceConfigurer} for chaining
* @throws Exception if configuration error happens
*/
PersistenceConfigurer<S, E> withPersistence() throws Exception;
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public class DefaultPersistenceConfigurer<S, E> extends
AnnotationConfigurerAdapter<ConfigurationData<S, E>, StateMachineConfigurationConfigurer<S, E>, StateMachineConfigurationBuilder<S, E>>
implements PersistenceConfigurer<S, E> {
private StateMachineRuntimePersister<S, E> persister;
@Override
public void configure(StateMachineConfigurationBuilder<S, E> builder) throws Exception {
builder.setStateMachineRuntimePersister(persister);
}
@Override
public PersistenceConfigurer<S, E> runtimePersister(StateMachineRuntimePersister<S, E> persister) {
this.persister = persister;
return this;
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public interface PersistenceConfigurer<S, E> extends
AnnotationConfigurerBuilder<StateMachineConfigurationConfigurer<S, E>> {
/**
* Specify a state machine runtime persister.
*
* @param persister the state machine runtime persister
* @return configurer for chaining
*/
PersistenceConfigurer<S, E> runtimePersister(StateMachineRuntimePersister<S, E> persister);
}

View File

@@ -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<S, E> {
private final SecurityRule eventSecurityRule;
private final SecurityRule transitionSecurityRule;
private final StateMachineMonitor<S, E> stateMachineMonitor;
private final List<StateMachineInterceptor<S, E>> interceptors;
/**
* Instantiates a new state machine configuration config data.
*/
public ConfigurationData() {
this(null, new SyncTaskExecutor(), new ConcurrentTaskScheduler(), false, null, new ArrayList<StateMachineListener<S, E>>(), false,
null, null, null, null, true, new DefaultStateMachineModelVerifier<S, E>(), null, null);
null, null, null, null, true, new DefaultStateMachineModelVerifier<S, E>(), null, null, null);
}
/**
@@ -84,13 +86,15 @@ public class ConfigurationData<S, E> {
* @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<S, E> ensemble,
List<StateMachineListener<S, E>> listeners, boolean securityEnabled,
AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager,
SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled,
StateMachineModelVerifier<S, E> verifier, String machineId, StateMachineMonitor<S, E> stateMachineMonitor) {
StateMachineModelVerifier<S, E> verifier, String machineId, StateMachineMonitor<S, E> stateMachineMonitor,
List<StateMachineInterceptor<S, E>> interceptors) {
this.beanFactory = beanFactory;
this.taskExecutor = taskExecutor;
this.taskScheduler = taskScheduler;
@@ -106,6 +110,7 @@ public class ConfigurationData<S, E> {
this.verifier = verifier;
this.machineId = machineId;
this.stateMachineMonitor = stateMachineMonitor;
this.interceptors = interceptors;
}
public String getMachineId() {
@@ -237,4 +242,13 @@ public class ConfigurationData<S, E> {
public SecurityRule getTransitionSecurityRule() {
return transitionSecurityRule;
}
/**
* Gets the state machine interceptors.
*
* @return the state machine interceptors
*/
public List<StateMachineInterceptor<S, E>> getStateMachineInterceptors() {
return interceptors;
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public abstract class AbstractPersistingStateMachineInterceptor<S, E> extends StateMachineInterceptorAdapter<S, E>
implements StateMachinePersist<S, E, Object> {
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition, StateMachine<S, E> 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<S, E> state, Message<E> message, Transition<S, E> transition, StateMachine<S, E> 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<S, E> context, Object contextObj) throws Exception;
/**
* Read {@link StateMachineContext} from persistent store.
*
* @param contextObj the context object
* @return the state machine context
*/
public abstract StateMachineContext<S, E> 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<S, E> buildStateMachineContext(StateMachine<S, E> stateMachine, State<S, E> state) {
ExtendedState extendedState = new DefaultExtendedState();
extendedState.getVariables().putAll(stateMachine.getExtendedState().getVariables());
ArrayList<StateMachineContext<S, E>> childs = new ArrayList<StateMachineContext<S, E>>();
S id = null;
if (state.isSubmachineState()) {
id = getDeepState(state);
} else if (state.isOrthogonal()) {
Collection<Region<S, E>> regions = ((AbstractState<S, E>)state).getRegions();
for (Region<S, E> r : regions) {
StateMachine<S, E> rsm = (StateMachine<S, E>) r;
childs.add(buildStateMachineContext(rsm, state));
}
id = state.getId();
} else {
id = state.getId();
}
// building history state mappings
Map<S, S> historyStates = new HashMap<S, S>();
PseudoState<S, E> historyState = ((AbstractStateMachine<S, E>) stateMachine).getHistoryState();
if (historyState != null) {
historyStates.put(null, ((HistoryPseudoState<S, E>)historyState).getState().getId());
}
Collection<State<S, E>> states = stateMachine.getStates();
for (State<S, E> ss : states) {
if (ss.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>) ss).getSubmachine();
PseudoState<S, E> ps = ((AbstractStateMachine<S, E>) submachine).getHistoryState();
if (ps != null) {
State<S, E> pss = ((HistoryPseudoState<S, E>)ps).getState();
if (pss != null) {
historyStates.put(ss.getId(), pss.getId());
}
}
}
}
return new DefaultStateMachineContext<S, E>(childs, id, null, null, extendedState, historyStates, stateMachine.getId());
}
private S getDeepState(State<S, E> state) {
Collection<S> 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];
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public interface StateMachineRuntimePersister<S, E> {
/**
* Gets a {@link StateMachineInterceptor} handling machine persistence.
*
* @return the interceptor handling persistence
*/
StateMachineInterceptor<S, E> getInterceptor();
}

View File

@@ -56,7 +56,7 @@ public class StateMachineModelTests {
ConfigurationData<String, String> 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<String, String>> stateData = new ArrayList<>();
StateData<String, String> stateData1 = new StateData<String, String>(null, null, "S1", null, null, null);

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public class JpaPersistingStateMachineInterceptor<S, E> extends AbstractPersistingStateMachineInterceptor<S, E>
implements StateMachinePersist<S, E, Object>, StateMachineRuntimePersister<S, E> {
private final JpaRepositoryStateMachinePersist<S, E> 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<S, E>(jpaStateMachineRepository);
}
/**
* Instantiates a new jpa persisting state machine interceptor.
*
* @param persist the persist
*/
public JpaPersistingStateMachineInterceptor(JpaRepositoryStateMachinePersist<S, E> persist) {
Assert.notNull(persist, "'persist' must be set");
this.persist = persist;
}
@Override
public StateMachineInterceptor<S, E> getInterceptor() {
return this;
}
@Override
public void write(StateMachineContext<S, E> context, Object contextObj) throws Exception {
persist.write(context, contextObj);
}
@Override
public StateMachineContext<S, E> read(Object contextObj) throws Exception {
return persist.read(contextObj);
}
}

View File

@@ -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;
}
}

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public class JpaRepositoryStateMachinePersist<S, E> extends RepositoryStateMachinePersist<JpaRepositoryStateMachine, S, E> {
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<JpaRepositoryStateMachine> getRepository() {
return jpaStateMachineRepository;
}
@Override
protected JpaRepositoryStateMachine build(StateMachineContext<S, E> context, byte[] serialisedContext) {
JpaRepositoryStateMachine jpaRepositoryStateMachine = new JpaRepositoryStateMachine();
jpaRepositoryStateMachine.setMachineId(context.getId());
jpaRepositoryStateMachine.setState(context.getState().toString());
jpaRepositoryStateMachine.setStateMachineContext(serialisedContext);
return jpaRepositoryStateMachine;
}
}

View File

@@ -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<JpaRepositoryStateMachine>{
}

View File

@@ -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<String, String> 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<PersistTestStates, PersistTestEvents> 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<? extends RepositoryState> statesRepository4;
@Autowired
JpaStateMachineRepository jpaStateMachineRepository1;
@Autowired
StateMachineRepository<JpaRepositoryStateMachine> jpaStateMachineRepository2;
}
@Configuration
@EnableStateMachine
public static class ConfigWithStrings extends StateMachineConfigurerAdapter<String, String> {
@Autowired
private JpaStateMachineRepository jpaStateMachineRepository;
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config
.withConfiguration()
.machineId("xxx1")
.and()
.withPersistence()
.runtimePersister(stateMachineRuntimePersister());
}
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1")
.and()
.withExternal()
.source("S2")
.target("S1")
.event("E2");
}
@Bean
public StateMachineRuntimePersister<String, String> stateMachineRuntimePersister() {
return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}
@Configuration
@EnableStateMachine
public static class ConfigWithEnums extends StateMachineConfigurerAdapter<PersistTestStates, PersistTestEvents> {
@Autowired
private JpaStateMachineRepository jpaStateMachineRepository;
@Override
public void configure(StateMachineConfigurationConfigurer<PersistTestStates, PersistTestEvents> config) throws Exception {
config
.withConfiguration()
.machineId("xxx2")
.and()
.withPersistence()
.runtimePersister(stateMachineRuntimePersister());
}
@Override
public void configure(StateMachineStateConfigurer<PersistTestStates, PersistTestEvents> states) throws Exception {
states
.withStates()
.initial(PersistTestStates.S1)
.state(PersistTestStates.S2);
}
@Override
public void configure(StateMachineTransitionConfigurer<PersistTestStates, PersistTestEvents> 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<PersistTestStates, PersistTestEvents> stateMachineRuntimePersister() {
return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
}
public enum PersistTestStates {
S1, S2;
}
public enum PersistTestEvents {
E1, E2;
}
}

View File

@@ -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();
}

View File

@@ -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<String, String> build(String machineId) {
ConfigurationData<String, String> configurationData = new ConfigurationData<>();
Collection<StateData<String, String>> stateDatas = new ArrayList<>();
for (RepositoryState s : stateRepository.findByMachineId(machineId == null ? "" : machineId)) {
@@ -284,8 +281,7 @@ public class RepositoryStateMachineModelFactory extends AbstractStateMachineMode
TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData, choicesCopy, junctionsCopy, forks, joins,
entrys, exits, historys);
StateMachineModel<String, String> stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData, transitionsData);
return stateMachineModel;
return new DefaultStateMachineModel<>(null, statesData, transitionsData);
}
private Guard<String, String> resolveGuard(RepositoryTransition t) {

View File

@@ -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 <S> the type of state
* @param <E> the type of event
* @param <M> the type of entity
*/
public abstract class RepositoryStateMachinePersist<M extends RepositoryStateMachine, S, E> implements StateMachinePersist<S, E, Object> {
private final Log log = LogFactory.getLog(RepositoryStateMachinePersist.class);
private final KryoSerialisationService serialisationService = new KryoSerialisationService();
@Override
public void write(StateMachineContext<S, E> 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<S, E> 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<M> 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<S, E> context, byte[] serialisedContext);
}

View File

@@ -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<M extends RepositoryStateMachine> extends CrudRepository<M, String> {
}

View File

@@ -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<Kryo> kryoThreadLocal = new ThreadLocal<Kryo>() {
@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 <S> the generic type
* @param <E> the element type
* @param context the context
* @return the byte[]
*/
public <S, E> byte[] serializeStateMachineContext(StateMachineContext<S, E> 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 <S> the generic type
* @param <E> the element type
* @param data the data
* @return the state machine context
*/
@SuppressWarnings("unchecked")
public <S, E> StateMachineContext<S, E> 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);
}
}

View File

@@ -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 {

View File

@@ -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[]

View File

@@ -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<States, Events> {
@Autowired
private JpaStateMachineRepository jpaStateMachineRepository;
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
throws Exception {
config
.withPersistence()
.runtimePersister(stateMachineruntimePersister());
}
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.S1)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> 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<States, Events> 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[]
}

View File

@@ -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<States, Events> stateMachineFactory;
@Autowired
private StateMachinePersist<States, Events, String> stateMachinePersist;
private StateMachine<States, Events> 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> events,
@RequestParam(value = "machine", required = false, defaultValue = MACHINE_ID_1) String machine,
Model model) throws Exception {
StateMachine<States, Events> stateMachine = getStateMachine(machine);
if (events != null) {
for (Events event : events) {
stateMachine.sendEvent(event);
}
}
StateMachineContext<States, Events> 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<States, Events> 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<States, Events> buildStateMachine(String machineId) throws Exception {
StateMachine<States, Events> stateMachine = stateMachineFactory.getStateMachine(machineId);
stateMachine.addStateListener(listener);
listener.resetMessages();
return restoreStateMachine(stateMachine, stateMachinePersist.read(machineId));
}
private StateMachine<States, Events> restoreStateMachine(StateMachine<States, Events> stateMachine,
StateMachineContext<States, Events> stateMachineContext) {
if (stateMachineContext == null) {
return stateMachine;
}
stateMachine.stop();
stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<States, Events>>() {
@Override
public void apply(StateMachineAccess<States, Events> function) {
function.resetStateMachine(stateMachineContext);
}
});
return stateMachine;
}
private Events[] getEvents() {
return EnumSet.allOf(Events.class).toArray(new Events[0]);
}
private String createMessages(List<String> messages) {
StringBuilder buf = new StringBuilder();
for (String message : messages) {
buf.append(message);
buf.append("\n");
}
return buf.toString();
}
}

View File

@@ -0,0 +1,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<States, Events> {
private final LinkedList<String> messages = new LinkedList<String>();
public List<String> getMessages() {
return messages;
}
public void resetMessages() {
messages.clear();
}
@Override
public void stateContext(StateContext<States, Events> stateContext) {
if (stateContext.getStage() == Stage.STATE_ENTRY) {
messages.addFirst("Enter " + stateContext.getTarget().getId());
} else if (stateContext.getStage() == Stage.STATE_EXIT) {
messages.addFirst("Exit " + stateContext.getSource().getId());
} else if (stateContext.getStage() == Stage.STATEMACHINE_START) {
messages.addLast("Machine started");
} else if (stateContext.getStage() == Stage.STATEMACHINE_STOP) {
messages.addFirst("Machine stopped");
}
}
}

View File

@@ -0,0 +1,6 @@
logging:
level:
root: INFO
security:
basic:
enabled: false

View File

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

View File

@@ -0,0 +1,107 @@
/*
* 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 static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import demo.datajpapersist.Application;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
@WebAppConfiguration
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class DataJpaPersistTests {
private MockMvc mvc;
@Autowired
private WebApplicationContext context;
@Test
public void testHome() throws Exception {
mvc.
perform(get("/state")).
andExpect(status().isOk());
}
@Test
public void testSendEventE1() throws Exception {
mvc.
perform(get("/state").param("events", "E1")).
andExpect(status().isOk()).
andExpect(content().string(containsString("Exit S1")));
}
@Test
public void testSendEventsE1E2() throws Exception {
mvc.
perform(get("/state").param("events", "E1").param("events", "E2")).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Exit S1"),
containsString("Exit S2"))));
}
@Test
public void testSendEventE4() throws Exception {
mvc.
perform(get("/state").param("events", "E1").param("machine", StateMachineController.MACHINE_ID_2)).
andExpect(status().isOk()).
andExpect(content().string(containsString("Exit S1")));
}
@Test
public void testChangeMachineRestores() throws Exception {
mvc.
perform(get("/state").param("events", "E1")).
andExpect(status().isOk()).
andExpect(content().string(containsString("Exit S1")));
mvc.
perform(get("/state").param("events", "E1").param("machine", StateMachineController.MACHINE_ID_2)).
andExpect(status().isOk()).
andExpect(content().string(containsString("Exit S1")));
mvc.
perform(get("/state").param("events", "E2").param("machine", StateMachineController.MACHINE_ID_1)).
andExpect(status().isOk()).
andExpect(content().string(containsString("Exit S2")));
mvc.
perform(get("/state").param("events", "E2").param("machine", StateMachineController.MACHINE_ID_2)).
andExpect(status().isOk()).
andExpect(content().string(containsString("Exit S2")));
}
@Before
public void setup() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(context).build();
}
}