Add history support for persist
- Enhance StateMachineContext to contain possible history state mappings for substates. - Add persist support for AbstractStateMachinePersister. - Modify some AbstractStateMachine functionality for history handling which then allows to also reset history state. - Relates to #182
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Copyright 2015-2016 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.
|
||||
@@ -49,6 +49,13 @@ public interface StateMachineContext<S, E> {
|
||||
*/
|
||||
E getEvent();
|
||||
|
||||
/**
|
||||
* Gets the history state mappings
|
||||
*
|
||||
* @return the history state mappings
|
||||
*/
|
||||
Map<S, S> getHistoryStates();
|
||||
|
||||
/**
|
||||
* Gets the event headers.
|
||||
*
|
||||
@@ -62,5 +69,4 @@ public interface StateMachineContext<S, E> {
|
||||
* @return the extended state
|
||||
*/
|
||||
ExtendedState getExtendedState();
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.statemachine.persist;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.statemachine.ExtendedState;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
@@ -26,7 +28,10 @@ import org.springframework.statemachine.access.StateMachineAccess;
|
||||
import org.springframework.statemachine.access.StateMachineFunction;
|
||||
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.util.Assert;
|
||||
@@ -105,7 +110,22 @@ public abstract class AbstractStateMachinePersister<S, E, T> implements StateMac
|
||||
id = state.getId();
|
||||
}
|
||||
|
||||
return new DefaultStateMachineContext<S, E>(childs, id, null, null,
|
||||
extendedState);
|
||||
// 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> hh = ((AbstractStateMachine<S, E>) submachine).getHistoryState();
|
||||
if (hh != null) {
|
||||
historyStates.put(ss.getId(), ((HistoryPseudoState<S, E>)hh).getState().getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new DefaultStateMachineContext<S, E>(childs, id, null, null, extendedState, historyStates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,10 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
public PseudoState<S, E> getHistoryState() {
|
||||
return history;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendEvent(Message<E> event) {
|
||||
if (hasStateMachineError()) {
|
||||
@@ -509,10 +513,41 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
for (State<S, E> ss : s.getStates()) {
|
||||
if (state != null && ss.getIds().contains(state)) {
|
||||
currentState = s;
|
||||
|
||||
// setting history for 'this' machine
|
||||
if (history != null && stateMachineContext.getHistoryStates() != null) {
|
||||
State<S, E> h = null;
|
||||
for (State<S, E> hh : getStates()) {
|
||||
if (hh.getId().equals(stateMachineContext.getHistoryStates().get(null))) {
|
||||
h = hh;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (h != null) {
|
||||
((HistoryPseudoState<S, E>)history).setState(h);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: not sure about starting submachine/regions here, though
|
||||
// needed if we only transit to super state or reset regions
|
||||
if (s.isSubmachineState()) {
|
||||
StateMachine<S, E> submachine = ((AbstractState<S, E>)s).getSubmachine();
|
||||
|
||||
// setting history for submachine state machine
|
||||
PseudoState<S, E> submachineHistory = ((AbstractStateMachine<S, E>)submachine).getHistoryState();
|
||||
if (submachineHistory != null) {
|
||||
State<S, E> h = null;
|
||||
for (State<S, E> hh : submachine.getStates()) {
|
||||
if (hh.getId().equals(stateMachineContext.getHistoryStates().get(s.getId()))) {
|
||||
h = hh;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (h != null) {
|
||||
((HistoryPseudoState<S, E>)submachineHistory).setState(h);
|
||||
}
|
||||
}
|
||||
|
||||
for (final StateMachineContext<S, E> child : stateMachineContext.getChilds()) {
|
||||
submachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S,E>>() {
|
||||
|
||||
@@ -826,7 +861,12 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
}
|
||||
}
|
||||
}
|
||||
if (history != null) {
|
||||
if (history != null && transition.getKind() != TransitionKind.INITIAL) {
|
||||
// do not set history if this is initial transition as
|
||||
// it would break history state set via reset as
|
||||
// we get here i.e. when machine is started in reset.
|
||||
// and it really doesn't make sense to set initial state for history
|
||||
// if we get here via initial transition
|
||||
if (history.getKind() == PseudoStateKind.HISTORY_SHALLOW) {
|
||||
((HistoryPseudoState<S, E>)history).setState(findDeep);
|
||||
} else if (history.getKind() == PseudoStateKind.HISTORY_DEEP){
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Copyright 2015-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.statemachine.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -34,6 +35,7 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
|
||||
|
||||
private final List<StateMachineContext<S, E>> childs;
|
||||
private final S state;
|
||||
private final Map<S, S> historyStates;
|
||||
private final E event;
|
||||
private final Map<String, Object> eventHeaders;
|
||||
private final ExtendedState extendedState;
|
||||
@@ -50,6 +52,20 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
|
||||
this(new ArrayList<StateMachineContext<S, E>>(), state, event, eventHeaders, extendedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new default state machine context.
|
||||
*
|
||||
* @param state the state
|
||||
* @param event the event
|
||||
* @param eventHeaders the event headers
|
||||
* @param extendedState the extended state
|
||||
* @param historyStates the history state mappings
|
||||
*/
|
||||
public DefaultStateMachineContext(S state, E event, Map<String, Object> eventHeaders, ExtendedState extendedState,
|
||||
Map<S, S> historyStates) {
|
||||
this(new ArrayList<StateMachineContext<S, E>>(), state, event, eventHeaders, extendedState, historyStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new default state machine context.
|
||||
*
|
||||
@@ -61,11 +77,27 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
|
||||
*/
|
||||
public DefaultStateMachineContext(List<StateMachineContext<S, E>> childs, S state, E event,
|
||||
Map<String, Object> eventHeaders, ExtendedState extendedState) {
|
||||
this(childs, state, event, eventHeaders, extendedState, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new default state machine context.
|
||||
*
|
||||
* @param childs the child state machine contexts
|
||||
* @param state the state
|
||||
* @param event the event
|
||||
* @param eventHeaders the event headers
|
||||
* @param extendedState the extended state
|
||||
* @param historyStates the history state mappings
|
||||
*/
|
||||
public DefaultStateMachineContext(List<StateMachineContext<S, E>> childs, S state, E event,
|
||||
Map<String, Object> eventHeaders, ExtendedState extendedState, Map<S, S> historyStates) {
|
||||
this.childs = childs;
|
||||
this.state = state;
|
||||
this.event = event;
|
||||
this.eventHeaders = eventHeaders;
|
||||
this.extendedState = extendedState;
|
||||
this.historyStates = historyStates != null ? historyStates : new HashMap<S, S>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,6 +110,11 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<S, S> getHistoryStates() {
|
||||
return historyStates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E getEvent() {
|
||||
return event;
|
||||
@@ -95,8 +132,7 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultStateMachineContext [childs=" + childs + ", state=" + state + ", event=" + event
|
||||
return "DefaultStateMachineContext [childs=" + childs + ", state=" + state + ", historyStates=" + historyStates + ", event=" + event
|
||||
+ ", eventHeaders=" + eventHeaders + ", extendedState=" + extendedState + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.statemachine.persist;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -30,10 +31,13 @@ import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.StateMachineContext;
|
||||
import org.springframework.statemachine.StateMachinePersist;
|
||||
import org.springframework.statemachine.StateMachineSystemConstants;
|
||||
import org.springframework.statemachine.TestUtils;
|
||||
import org.springframework.statemachine.config.EnableStateMachine;
|
||||
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
import org.springframework.statemachine.config.configurers.StateConfigurer.History;
|
||||
import org.springframework.statemachine.state.HistoryPseudoState;
|
||||
|
||||
public class StateMachinePersistTests extends AbstractStateMachineTests {
|
||||
|
||||
@@ -196,6 +200,75 @@ public class StateMachinePersistTests extends AbstractStateMachineTests {
|
||||
assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S12", "S22", "S221"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testHistoryFlatShallow() throws Exception {
|
||||
// not sure if this test makes any sense as it was done for
|
||||
// gh182, but persisting history state which is transitient
|
||||
// is not exactly possible
|
||||
context.register(Config61.class, Config62.class);
|
||||
context.refresh();
|
||||
|
||||
InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1();
|
||||
StateMachinePersister<String, String, String> persister = new DefaultStateMachinePersister<>(stateMachinePersist);
|
||||
|
||||
StateMachine<String, String> stateMachine1 = context.getBean("machine1", StateMachine.class);
|
||||
StateMachine<String, String> stateMachine2 = context.getBean("machine2", StateMachine.class);
|
||||
// start gets in state S1
|
||||
stateMachine1.start();
|
||||
|
||||
// event E1 takes into state S2
|
||||
stateMachine1.sendEvent("E1");
|
||||
assertThat(stateMachine1.getState().getIds(), contains("S2"));
|
||||
Object history = readHistoryState(stateMachine1);
|
||||
assertThat(history, is("S2"));
|
||||
|
||||
// we persist with state S2 and history keeps same S2
|
||||
persister.persist(stateMachine1, "xxx");
|
||||
|
||||
stateMachine2 = persister.restore(stateMachine2, "xxx");
|
||||
history = readHistoryState(stateMachine2);
|
||||
assertThat(history, is("S2"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testHistorySubShallow() throws Exception {
|
||||
// not sure if this test makes any sense as it was done for
|
||||
// gh182, but persisting history state which is transitient
|
||||
// is not exactly possible
|
||||
context.register(Config71.class, Config72.class);
|
||||
context.refresh();
|
||||
|
||||
InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1();
|
||||
StateMachinePersister<String, String, String> persister = new DefaultStateMachinePersister<>(stateMachinePersist);
|
||||
|
||||
StateMachine<String, String> stateMachine1 = context.getBean("machine1", StateMachine.class);
|
||||
StateMachine<String, String> stateMachine2 = context.getBean("machine2", StateMachine.class);
|
||||
stateMachine1.start();
|
||||
|
||||
stateMachine1.sendEvent("E1");
|
||||
assertThat(stateMachine1.getState().getIds(), containsInAnyOrder("S2", "S21"));
|
||||
|
||||
stateMachine1.sendEvent("E3");
|
||||
assertThat(stateMachine1.getState().getIds(), containsInAnyOrder("S2", "S22"));
|
||||
persister.persist(stateMachine1, "xxx");
|
||||
|
||||
stateMachine1.sendEvent("E2");
|
||||
assertThat(stateMachine1.getState().getIds(), containsInAnyOrder("S1"));
|
||||
|
||||
|
||||
stateMachine2 = persister.restore(stateMachine2, "xxx");
|
||||
Object history = TestUtils.readField("history", stateMachine1);
|
||||
assertThat(history, notNullValue());
|
||||
assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S2", "S22"));
|
||||
|
||||
stateMachine2.sendEvent("E2");
|
||||
assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S1"));
|
||||
stateMachine2.sendEvent("EH3");
|
||||
assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S2", "S22"));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config1 extends StateMachineConfigurerAdapter<String, String> {
|
||||
@@ -417,6 +490,113 @@ public class StateMachinePersistTests extends AbstractStateMachineTests {
|
||||
static class Config52 extends Config5 {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine(name = "machine1")
|
||||
static class Config61 extends Config6 {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine(name = "machine2")
|
||||
static class Config62 extends Config6 {
|
||||
}
|
||||
|
||||
static class Config6 extends StateMachineConfigurerAdapter<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("S1")
|
||||
.state("S1")
|
||||
.state("S2")
|
||||
.state("S3")
|
||||
.state("S4")
|
||||
.history("SH", History.SHALLOW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source("S1").target("S2").event("E1")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S2").target("S3").event("E2")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S1").target("S4").event("E3")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S2").target("S4").event("E3")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S3").target("S4").event("E3")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S1").target("SH").event("EH")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S2").target("SH").event("EH")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S3").target("SH").event("EH");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine(name = "machine1")
|
||||
static class Config71 extends Config7 {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine(name = "machine2")
|
||||
static class Config72 extends Config7 {
|
||||
}
|
||||
|
||||
static class Config7 extends StateMachineConfigurerAdapter<String, String> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial("S1")
|
||||
.state("S1")
|
||||
.state("S2")
|
||||
.history("SH", History.SHALLOW)
|
||||
.and()
|
||||
.withStates()
|
||||
.parent("S2")
|
||||
.initial("S21")
|
||||
.state("S22")
|
||||
.history("S2H", History.SHALLOW);
|
||||
}
|
||||
|
||||
@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")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S21").target("S22").event("E3")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S1").target("SH").event("EH1")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S2").target("SH").event("EH2")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S1").target("S2H").event("EH3")
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("S2").target("S2H").event("EH3");
|
||||
}
|
||||
}
|
||||
|
||||
static class InMemoryStateMachinePersist1 implements StateMachinePersist<String, String, String> {
|
||||
|
||||
private final HashMap<String, StateMachineContext<String, String>> contexts = new HashMap<>();
|
||||
@@ -447,4 +627,14 @@ public class StateMachinePersistTests extends AbstractStateMachineTests {
|
||||
}
|
||||
}
|
||||
|
||||
private static Object readHistoryState(Object stateMachine) throws Exception {
|
||||
Object pseudo = TestUtils.readField("history", stateMachine);
|
||||
if (pseudo instanceof HistoryPseudoState) {
|
||||
Object state = TestUtils.readField("state", pseudo);
|
||||
if (state != null) {
|
||||
return TestUtils.callMethod("getId", state);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user