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:
Janne Valkealahti
2016-03-12 15:52:36 +00:00
parent f389244862
commit f72e4dad9e
5 changed files with 300 additions and 8 deletions

View File

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

View File

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

View File

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

View File

@@ -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 + "]";
}
}

View File

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