From bd2518cce928b4bc77559cea16bca1aac0b763eb Mon Sep 17 00:00:00 2001 From: Hamid Mortazavi Date: Sun, 7 Apr 2019 12:27:10 +0100 Subject: [PATCH] Persisting StateMachineContext for SM with nested regions and Fork/Join - fixing issue when StateMachineContext is incorrectly persisted for SM with nested regions and Fork/Join configuration - Fixes #723 --- .../AbstractStateMachinePersister.java | 16 +-- .../DefaultStateMachinePersisterTests.java | 130 +++++++++++++++++- 2 files changed, 129 insertions(+), 17 deletions(-) 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 5cc1957a..8b0a7f53 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 @@ -88,7 +88,9 @@ public abstract class AbstractStateMachinePersister implements StateMac S id = null; State state = stateMachine.getState(); if (state.isSubmachineState()) { - id = getDeepState(state); + StateMachine submachine = ((AbstractState) state).getSubmachine(); + id = submachine.getState().getId(); + childs.add(buildStateMachineContext(submachine)); } else if (state.isOrthogonal()) { Collection> regions = ((AbstractState)state).getRegions(); for (Region r : regions) { @@ -103,8 +105,8 @@ public abstract class AbstractStateMachinePersister implements StateMac // building history state mappings Map historyStates = new HashMap(); PseudoState historyState = ((AbstractStateMachine) stateMachine).getHistoryState(); - if (historyState != null) { - historyStates.put(null, ((HistoryPseudoState)historyState).getState().getId()); + if (historyState != null && ((HistoryPseudoState)historyState).getState() != null) { + historyStates.put(null, ((HistoryPseudoState) historyState).getState().getId()); } Collection> states = stateMachine.getStates(); for (State ss : states) { @@ -121,12 +123,4 @@ public abstract class AbstractStateMachinePersister implements StateMac } return new DefaultStateMachineContext(childs, id, null, null, extendedState, historyStates, stateMachine.getId()); } - - private S getDeepState(State state) { - Collection ids1 = state.getIds(); - @SuppressWarnings("unchecked") - S[] ids2 = (S[]) ids1.toArray(); - // TODO: can this be empty as then we'd get error? - return ids2[ids2.length-1]; - } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/DefaultStateMachinePersisterTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/DefaultStateMachinePersisterTests.java index f9509354..af121010 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/DefaultStateMachinePersisterTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/DefaultStateMachinePersisterTests.java @@ -73,19 +73,19 @@ public class DefaultStateMachinePersisterTests { persister.persist(machine, "xxx"); context = persist.contexts.get("xxx"); assertThat(context.getState(), is("S1I")); - assertThat(context.getChilds().isEmpty(), is(true)); + assertThat(context.getChilds().size(), is(1)); machine.sendEvent("E2"); persister.persist(machine, "xxx"); context = persist.contexts.get("xxx"); - assertThat(context.getState(), is("S11I")); - assertThat(context.getChilds().isEmpty(), is(true)); + assertThat(context.getState(), is("S11")); + assertThat(context.getChilds().size(), is(1)); machine.sendEvent("E3"); persister.persist(machine, "xxx"); context = persist.contexts.get("xxx"); - assertThat(context.getState(), is("S111")); - assertThat(context.getChilds().isEmpty(), is(true)); + assertThat(context.getState(), is("S11")); + assertThat(context.getChilds().size(), is(1)); } @Test @@ -125,6 +125,53 @@ public class DefaultStateMachinePersisterTests { assertThat(context.getChilds().get(1).getState(), anyOf(is("S12"), is("S222"))); } + @Test + public void testDeepNestedRegionsAndFork() throws Exception { + StateMachine machine = buildDeepNestedRegionsAndFork(); + + machine.start(); + InMemoryStateMachinePersist1 persist = new InMemoryStateMachinePersist1(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(persist); + persister.persist(machine, "xxx"); + StateMachineContext context = persist.contexts.get("xxx"); + assertThat(context.getState(), is("S2")); + + machine.sendEvent("E1"); + persister.persist(machine, "xxx"); + context = persist.contexts.get("xxx"); + assertThat(context.getState(), is("S3")); + assertThat(context.getChilds().size(), is(1)); + assertThat(context.getChilds().get(0).getChilds().size(), is(2)); + + machine.sendEvent("E2"); + persister.persist(machine, "xxx"); + context = persist.contexts.get("xxx"); + assertThat(context.getState(), is("S3")); + assertThat(context.getChilds().size(), is(1)); + assertThat(context.getChilds().get(0).getChilds().size(), is(2)); + + machine.sendEvent("E3"); + persister.persist(machine, "xxx"); + context = persist.contexts.get("xxx"); + assertThat(context.getState(), is("S3")); + assertThat(context.getChilds().size(), is(1)); + assertThat(context.getChilds().get(0).getChilds().size(), is(2)); + + machine.sendEvent("E4"); + persister.persist(machine, "xxx"); + context = persist.contexts.get("xxx"); + assertThat(context.getState(), is("S3")); + assertThat(context.getChilds().size(), is(1)); + assertThat(context.getChilds().get(0).getChilds().size(), is(2)); + + machine.sendEvent("E5"); + persister.persist(machine, "xxx"); + context = persist.contexts.get("xxx"); + assertThat(context.getState(), is("END")); + assertThat(context.getChilds().size(), is(1)); + assertThat(context.getChilds().get(0).getChilds().isEmpty(), is(true)); + } + private StateMachine buildSimpleFlat() throws Exception { Builder builder = StateMachineBuilder.builder(); builder.configureStates() @@ -170,7 +217,6 @@ public class DefaultStateMachinePersisterTests { .source("S11I") .target("S111") .event("E3"); - return builder.build(); } @@ -218,6 +264,78 @@ public class DefaultStateMachinePersisterTests { return builder.build(); } + private StateMachine buildDeepNestedRegionsAndFork() throws Exception { + Builder builder = StateMachineBuilder.builder(); + builder.configureStates() + .withStates() + .initial("S1") + .and() + .withStates() + .parent("S1") + .initial("S2") + .state("S22") + .fork("F1") + .state("S3") + .join("J1") + .end("END") + .and() + .withStates() + .parent("S3") + .initial("S4") + .state("S41") + .end("S4E") + .and() + .withStates() + .parent("S3") + .initial("S5") + .state("S51") + .end("S5E"); + + builder.configureTransitions() + .withExternal() + .source("S2") + .target("S22") + .event("E1") + .and() + .withExternal() + .source("S22") + .target("F1") + .and() + .withFork() + .source("F1") + .target("S3") + .and() + .withExternal() + .source("S4") + .target("S41") + .event("E2") + .and() + .withExternal() + .source("S41") + .target("S4E") + .event("E3") + .and() + .withExternal() + .source("S5") + .target("S51") + .event("E4") + .and() + .withExternal() + .source("S51") + .target("S5E") + .event("E5") + .and() + .withJoin() + .source("S3") + .target("J1") + .and() + .withExternal() + .source("J1") + .target("END"); + + return builder.build(); + } + static class InMemoryStateMachinePersist1 implements StateMachinePersist { public final HashMap> contexts = new HashMap<>();