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 572d49ff..84ae1636 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 @@ -86,6 +86,10 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo private volatile State currentState; + // using this to log last state when machine stops, as + // it's a bit difficult to keep currentState non-null after stop. + private volatile State lastState; + private volatile Exception currentError; private volatile PseudoState history; @@ -158,7 +162,13 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo @Override public State getState() { - return currentState; + // if we're complete assume we're stopped + // and state was stashed into lastState + if (isComplete()) { + return lastState; + } else { + return currentState; + } } @Override @@ -219,7 +229,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo Assert.state(initialState.getPseudoState() != null && initialState.getPseudoState().getKind() == PseudoStateKind.INITIAL, "Initial state's pseudostate kind must be INITIAL"); - + lastState = null; extendedState.setExtendedStateChangeListener(new ExtendedStateChangeListener() { @Override public void changed(Object key, Object value) { @@ -333,6 +343,9 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo protected void doStop() { stateMachineExecutor.stop(); notifyStateMachineStopped(buildStateContext(Stage.STATEMACHINE_STOP, null, null, this)); + // stash current state before we null it so that + // we can still return where we 'were' when machine is stopped + lastState = currentState; currentState = null; initialEnabled = null; } @@ -519,6 +532,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo for (State ss : s.getStates()) { if (state != null && ss.getIds().contains(state)) { currentState = s; + // setting lastState here is needed for restore + lastState = currentState; // TODO: not sure about starting submachine/regions here, though // needed if we only transit to super state or reset regions if (s.isSubmachineState()) { @@ -838,7 +853,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo if (currentState.isSubmachineState()) { StateMachine submachine = ((AbstractState)currentState).getSubmachine(); - if (submachine.getState() == state) { + // need to check complete as submachine may now return non null + if (!submachine.isComplete() && submachine.getState() == state) { if (currentState == findDeep) { if (isTargetSubOf) { entryToState(currentState, message, transition, stateMachine); @@ -897,6 +913,11 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo ((HistoryPseudoState)history).setState(state); } } + // if state was set from parent and we're now complete + // also initiate stop + if (stateMachine != this && isComplete()) { + stop(); + } } void exitCurrentState(State state, Message message, Transition transition, StateMachine stateMachine) { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java index b6eec51b..3b4437c5 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java @@ -33,7 +33,10 @@ 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.EnableStateMachineFactory; import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.StateMachineFactory; +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.config.configurers.StateConfigurer.History; @@ -269,6 +272,33 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S2", "S22")); } + @Test + public void testPersistWithEnd() throws Exception { + context.register(Config8.class); + context.refresh(); + InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + @SuppressWarnings("unchecked") + StateMachineFactory stateMachineFactory = context.getBean(StateMachineFactory.class); + + StateMachine stateMachine = stateMachineFactory.getStateMachine(); + assertThat(stateMachine, notNullValue()); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S1")); + + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2")); + assertThat(stateMachine.isComplete(), is(true)); + + persister.persist(stateMachine, "xxx"); + + stateMachine = stateMachineFactory.getStateMachine(); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S1")); + + stateMachine = persister.restore(stateMachine, "xxx"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2")); + assertThat(stateMachine.isComplete(), is(true)); + } + @Configuration @EnableStateMachine static class Config1 extends StateMachineConfigurerAdapter { @@ -597,6 +627,35 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { } } + @Configuration + @EnableStateMachineFactory + static class Config8 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .autoStartup(true); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .end("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + } + } + static class InMemoryStateMachinePersist1 implements StateMachinePersist { private final HashMap> contexts = new HashMap<>(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java index 8f69d3a7..a7adcecf 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/EndStateTests.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. @@ -65,6 +65,7 @@ public class EndStateTests extends AbstractStateMachineTests { assertThat(machine.isComplete(), is(false)); machine.sendEvent(TestEvents.EF); assertThat(machine.isComplete(), is(true)); + assertThat(machine.getState().getIds(), contains(TestStates.SF)); } @Test @@ -93,6 +94,32 @@ public class EndStateTests extends AbstractStateMachineTests { assertThat(machine.getState().getIds(), contains(TestStates3.READY)); } + @Test + public void testEndStateCompletesSubmachine() { + context.register(Config4.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + machine.start(); + assertThat(machine, notNullValue()); + assertThat(machine.isComplete(), is(false)); + assertThat(machine.getState().getIds(), contains(TestStates.SI)); + + machine.sendEvent(TestEvents.E1); + assertThat(machine.isComplete(), is(false)); + assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S11)); + + machine.sendEvent(TestEvents.E2); + assertThat(machine.isComplete(), is(false)); + assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.S12)); + + machine.sendEvent(TestEvents.E3); + assertThat(machine.isComplete(), is(false)); + assertThat(machine.getState().getIds(), contains(TestStates.S1, TestStates.SF)); + } + @Configuration @EnableStateMachine static class Config1 extends EnumStateMachineConfigurerAdapter { @@ -278,4 +305,46 @@ public class EndStateTests extends AbstractStateMachineTests { } + @Configuration + @EnableStateMachine + static class Config4 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.SI) + .state(TestStates.S1) + .and() + .withStates() + .parent(TestStates.S1) + .initial(TestStates.S11) + .state(TestStates.S12) + .end(TestStates.SF); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.SI) + .target(TestStates.S1) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S11) + .target(TestStates.S12) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S12) + .target(TestStates.SF) + .event(TestEvents.E3); + } + + @Bean + public TaskExecutor taskExecutor() { + return new SyncTaskExecutor(); + } + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java index 19277142..d7c0bc8b 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/TransitionTests.java @@ -18,7 +18,7 @@ package org.springframework.statemachine.transition; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -102,8 +102,9 @@ public class TransitionTests extends AbstractStateMachineTests { ObjectStateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); machine.start(); - // end state terminates sm so state is null - assertThat(machine.getState(), nullValue()); + // end state terminates sm so check machine still gives it + assertThat(machine.getState(), notNullValue()); + assertThat(machine.getState().getIds(), contains(TestStates.SF)); assertThat(machine.isComplete(), is(true)); assertThat(machine.isRunning(), is(false)); }