diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContext.java index 249829de..256474b9 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 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 { */ E getEvent(); + /** + * Gets the history state mappings + * + * @return the history state mappings + */ + Map getHistoryStates(); + /** * Gets the event headers. * @@ -62,5 +69,4 @@ public interface StateMachineContext { * @return the extended state */ ExtendedState getExtendedState(); - } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java index a5ab1d73..b7ef5f71 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java @@ -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 implements StateMac id = state.getId(); } - return new DefaultStateMachineContext(childs, id, null, null, - extendedState); + // building history state mappings + Map historyStates = new HashMap(); + PseudoState historyState = ((AbstractStateMachine) stateMachine).getHistoryState(); + if (historyState != null) { + historyStates.put(null, ((HistoryPseudoState)historyState).getState().getId()); + } + Collection> states = stateMachine.getStates(); + for (State ss : states) { + if (ss.isSubmachineState()) { + StateMachine submachine = ((AbstractState) ss).getSubmachine(); + PseudoState hh = ((AbstractStateMachine) submachine).getHistoryState(); + if (hh != null) { + historyStates.put(ss.getId(), ((HistoryPseudoState)hh).getState().getId()); + } + } + } + return new DefaultStateMachineContext(childs, id, null, null, extendedState, historyStates); } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index c221f13f..d71b4347 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -173,6 +173,10 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo this.history = history; } + public PseudoState getHistoryState() { + return history; + } + @Override public boolean sendEvent(Message event) { if (hasStateMachineError()) { @@ -509,10 +513,41 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo for (State ss : s.getStates()) { if (state != null && ss.getIds().contains(state)) { currentState = s; + + // setting history for 'this' machine + if (history != null && stateMachineContext.getHistoryStates() != null) { + State h = null; + for (State hh : getStates()) { + if (hh.getId().equals(stateMachineContext.getHistoryStates().get(null))) { + h = hh; + break; + } + } + if (h != null) { + ((HistoryPseudoState)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 submachine = ((AbstractState)s).getSubmachine(); + + // setting history for submachine state machine + PseudoState submachineHistory = ((AbstractStateMachine)submachine).getHistoryState(); + if (submachineHistory != null) { + State h = null; + for (State hh : submachine.getStates()) { + if (hh.getId().equals(stateMachineContext.getHistoryStates().get(s.getId()))) { + h = hh; + break; + } + } + if (h != null) { + ((HistoryPseudoState)submachineHistory).setState(h); + } + } + for (final StateMachineContext child : stateMachineContext.getChilds()) { submachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction>() { @@ -826,7 +861,12 @@ public abstract class AbstractStateMachine 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)history).setState(findDeep); } else if (history.getKind() == PseudoStateKind.HISTORY_DEEP){ diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineContext.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineContext.java index b6993c33..2a1427f3 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineContext.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 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 implements StateMachineContext> childs; private final S state; + private final Map historyStates; private final E event; private final Map eventHeaders; private final ExtendedState extendedState; @@ -50,6 +52,20 @@ public class DefaultStateMachineContext implements StateMachineContext>(), 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 eventHeaders, ExtendedState extendedState, + Map historyStates) { + this(new ArrayList>(), state, event, eventHeaders, extendedState, historyStates); + } + /** * Instantiates a new default state machine context. * @@ -61,11 +77,27 @@ public class DefaultStateMachineContext implements StateMachineContext> childs, S state, E event, Map 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> childs, S state, E event, + Map eventHeaders, ExtendedState extendedState, Map historyStates) { this.childs = childs; this.state = state; this.event = event; this.eventHeaders = eventHeaders; this.extendedState = extendedState; + this.historyStates = historyStates != null ? historyStates : new HashMap(); } @Override @@ -78,6 +110,11 @@ public class DefaultStateMachineContext implements StateMachineContext getHistoryStates() { + return historyStates; + } + @Override public E getEvent() { return event; @@ -95,8 +132,7 @@ public class DefaultStateMachineContext implements StateMachineContext persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + StateMachine stateMachine1 = context.getBean("machine1", StateMachine.class); + StateMachine 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 persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + StateMachine stateMachine1 = context.getBean("machine1", StateMachine.class); + StateMachine 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 { @@ -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 { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S1") + .state("S2") + .state("S3") + .state("S4") + .history("SH", History.SHALLOW); + } + + @Override + public void configure(StateMachineTransitionConfigurer 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 { + + @Override + public void configure(StateMachineStateConfigurer 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 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 { private final HashMap> 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; + } }