diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java index 31fd7ece..40ffa5c1 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java @@ -78,6 +78,7 @@ import org.springframework.statemachine.support.tree.TreeTraverser; import org.springframework.statemachine.support.tree.Tree.Node; import org.springframework.statemachine.transition.DefaultExternalTransition; import org.springframework.statemachine.transition.DefaultInternalTransition; +import org.springframework.statemachine.transition.DefaultLocalTransition; import org.springframework.statemachine.transition.InitialTransition; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.transition.TransitionKind; @@ -716,6 +717,15 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS transitionData.getSecurityRule(), transitionData.getErrorAction()); transitions.add(transition); + } else if (transitionData.getKind() == TransitionKind.LOCAL) { + // TODO can we do this? + if (stateMap.get(source) == null || stateMap.get(target) == null) { + continue; + } + DefaultLocalTransition transition = new DefaultLocalTransition(stateMap.get(source), + stateMap.get(target), transitionData.getActions(), event, transitionData.getGuard(), trigger, + transitionData.getSecurityRule(), transitionData.getErrorAction()); + transitions.add(transition); } else if (transitionData.getKind() == TransitionKind.INTERNAL) { DefaultInternalTransition transition = new DefaultInternalTransition(stateMap.get(source), transitionData.getActions(), event, transitionData.getGuard(), trigger, diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java index 957fb0b4..974391b7 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java @@ -207,6 +207,17 @@ public class StateMachineState extends AbstractState { ((StateMachineState) immediateDeepParent).getSubmachine().getStateMachineAccessor() .doWithRegion(new StateMachineFunction>() { + @Override + public void apply(StateMachineAccess function) { + function.setInitialEnabled(false); + } + }); + } + if (immediateDeepParent == null && getSubmachine().getStates().contains(target) && !isInitial(target) + && StateMachineUtils.isSubstate(context.getTransition().getSource(), context.getTransition().getTarget())) { + getSubmachine().getStateMachineAccessor().doWithRegion( + new StateMachineFunction>() { + @Override public void apply(StateMachineAccess function) { function.setInitialEnabled(false); 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 1d2d0d9f..db8a2a10 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 @@ -867,7 +867,6 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo synchronized void setCurrentState(State state, Message message, Transition transition, boolean exit, StateMachine stateMachine, Collection> sources, Collection> targets) { - State findDeep = findDeepParent(state); boolean isTargetSubOf = false; if (transition != null) { @@ -1026,6 +1025,12 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo boolean isSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), currentState); boolean isSubOfTarget = StateMachineUtils.isSubstate(transition.getTarget(), currentState); + if (transition.getKind() == TransitionKind.LOCAL && StateMachineUtils.isSubstate(transition.getSource(), transition.getTarget()) && transition.getSource() == currentState) { + return; + } else if (transition.getKind() == TransitionKind.LOCAL && StateMachineUtils.isSubstate(transition.getTarget(), transition.getSource()) && transition.getTarget() == currentState) { + return; + } + // TODO: this and entry below should be done via a separate // voter of some sort which would reveal transition path // we could make a choice on. @@ -1083,15 +1088,26 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo boolean isSubOfSource = StateMachineUtils.isSubstate(transition.getSource(), currentState); boolean isSubOfTarget = StateMachineUtils.isSubstate(transition.getTarget(), currentState); + + if (transition.getKind() == TransitionKind.LOCAL && StateMachineUtils.isSubstate(transition.getSource(), transition.getTarget()) + && transition.getSource() == currentState) { + return; + } else if (transition.getKind() == TransitionKind.LOCAL && StateMachineUtils.isSubstate(transition.getTarget(), transition.getSource()) + && transition.getTarget() == currentState) { + return; + } + if (currentState == transition.getSource() && currentState == transition.getTarget()) { } else if (!isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { } else if (isComingFromOtherSubmachine) { } else if (!isSubOfSource && !isSubOfTarget && findDeep2 == null) { } else if (isSubOfSource && !isSubOfTarget && currentState == transition.getTarget()) { - if (isDirectSubstate(transition.getSource(), transition.getTarget())) { + if (isDirectSubstate(transition.getSource(), transition.getTarget()) && transition.getKind() != TransitionKind.LOCAL + && isInitial(transition.getTarget())) { return; } - } else if (!isSubOfSource && !isSubOfTarget && (transition.getSource() == currentState && StateMachineUtils.isSubstate(currentState, transition.getTarget()))) { + } else if (!isSubOfSource && !isSubOfTarget + && (transition.getSource() == currentState && StateMachineUtils.isSubstate(currentState, transition.getTarget()))) { } else if (!isSubOfSource && !isSubOfTarget) { return; } @@ -1102,6 +1118,10 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo state.entry(stateContext); } + private static boolean isInitial(State state) { + return state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.INITIAL; + } + private static boolean isDirectSubstate(State left, State right) { // Checks if right hand side is a direct substate of a left hand side. if (left != null && left.isSubmachineState()) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultLocalTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultLocalTransition.java index cfd52626..ccb136e2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultLocalTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/DefaultLocalTransition.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. @@ -19,15 +19,22 @@ import java.util.Collection; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.guard.Guard; +import org.springframework.statemachine.security.SecurityRule; import org.springframework.statemachine.state.State; import org.springframework.statemachine.trigger.Trigger; public class DefaultLocalTransition extends AbstractLocalTransition { - public DefaultLocalTransition(State source, State target, Collection> actions, E event, Guard guard, Trigger trigger) { + public DefaultLocalTransition(State source, State target, Collection> actions, E event, Guard guard, + Trigger trigger) { super(source, target, actions, event, guard, trigger); } + public DefaultLocalTransition(State source, State target, Collection> actions, E event, + Guard guard, Trigger trigger, SecurityRule securityRule, Action errorAction) { + super(source, target, actions, event, guard, trigger, securityRule, errorAction); + } + @Override public String toString() { return "DefaultLocalTransition [getSource()=" + getSource() + ", getTarget()=" + getTarget() + "]"; diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java index 87e6cfb7..f397871f 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java @@ -351,7 +351,7 @@ public class SubStateMachineTests extends AbstractStateMachineTests { assertThat(entryActionS111.stateContexts.size(), is(2)); assertThat(exitActionS111.stateContexts.size(), is(1)); - assertThat(entryActionS11.stateContexts.size(), is(2)); + assertThat(entryActionS11.stateContexts.size(), is(1)); assertThat(exitActionS11.stateContexts.size(), is(1)); assertThat(entryActionS1.stateContexts.size(), is(1)); assertThat(exitActionS1.stateContexts.size(), is(0)); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/LocalTransitionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/LocalTransitionTests.java new file mode 100644 index 00000000..419d2b9e --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/transition/LocalTransitionTests.java @@ -0,0 +1,335 @@ +/* + * Copyright 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine.transition; + +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 static org.junit.Assert.assertTrue; + +import java.util.ArrayList; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; +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.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; + +@SuppressWarnings("unchecked") +public class LocalTransitionTests extends AbstractStateMachineTests { + + @Test + public void testExternalSuperDoesEntryExitToSub() { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + machine.sendEvent("E20"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S21")); + assertThat(listener.entered, containsInAnyOrder("S2", "S21")); + } + + @Test + public void testLocalSuperDoesNotEntryExitToSub() { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + machine.sendEvent("E30"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S21")); + assertThat(listener.entered, containsInAnyOrder("S21")); + } + + @Test + public void testExternalToNonInitialSuperDoesEntryExitToSub() { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + machine.sendEvent("E21"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S21")); + assertThat(listener.entered, containsInAnyOrder("S2", "S22")); + } + + @Test + public void testLocalToNonInitialSuperDoesNotEntryExitToSub() { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + machine.sendEvent("E31"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S21")); + assertThat(listener.entered, containsInAnyOrder("S22")); + } + + @Test + public void testExternalSuperDoesEntryExitToParent() { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + machine.sendEvent("E22"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S21")); + assertThat(listener.entered, containsInAnyOrder("S2", "S21")); + } + + @Test + public void testLocalSuperDoesNotEntryExitToParent() { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + machine.sendEvent("E32"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S21")); + assertThat(listener.entered, containsInAnyOrder("S21")); + } + + @Test + public void testExternalToNonInitialSuperDoesEntryExitToParent() { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + machine.sendEvent("E21"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S21")); + assertThat(listener.entered, containsInAnyOrder("S2", "S22")); + + listener.reset(); + machine.sendEvent("E23"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S22")); + assertThat(listener.entered, containsInAnyOrder("S2", "S22")); + } + + @Test + public void testLocalToNonInitialSuperDoesNotEntryExitToParent() { + context.register(Config1.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + machine.sendEvent("E31"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S21")); + assertThat(listener.entered, containsInAnyOrder("S22")); + + listener.reset(); + machine.sendEvent("E33"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S22")); + assertThat(listener.entered, containsInAnyOrder("S22")); + } + + @Configuration + @EnableStateMachine + static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2") + .and() + .withStates() + .parent("S2") + .initial("S21") + .state("S22"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S21") + .event("E20") + .and() + .withExternal() + .source("S2") + .target("S22") + .event("E21") + .and() + .withExternal() + .source("S21") + .target("S2") + .event("E22") + .and() + .withExternal() + .source("S22") + .target("S2") + .event("E23") + .and() + .withLocal() + .source("S2") + .target("S21") + .event("E30") + .and() + .withLocal() + .source("S2") + .target("S22") + .event("E31") + .and() + .withLocal() + .source("S21") + .target("S2") + .event("E32") + .and() + .withLocal() + .source("S22") + .target("S2") + .event("E33"); + } + + } + + private static class TestListener extends StateMachineListenerAdapter { + + final ArrayList entered = new ArrayList<>(); + final ArrayList exited = new ArrayList<>(); + + @Override + public void stateEntered(State state) { + entered.add(state.getId()); + } + + @Override + public void stateExited(State state) { + exited.add(state.getId()); + } + + public void reset() { + entered.clear(); + exited.clear(); + } + } + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } +} diff --git a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java index 24989a3d..fe81f508 100644 --- a/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java +++ b/spring-statemachine-uml/src/test/java/org/springframework/statemachine/uml/UmlStateMachineModelFactoryTests.java @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; +import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -45,7 +46,9 @@ import org.springframework.statemachine.config.model.StateMachineModel; import org.springframework.statemachine.config.model.StateMachineModelFactory; import org.springframework.statemachine.config.model.TransitionData; import org.springframework.statemachine.guard.Guard; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.state.PseudoStateKind; +import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.TransitionKind; import org.springframework.util.ObjectUtils; @@ -676,6 +679,198 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S3")); } + @Test + @SuppressWarnings("unchecked") + public void testSimpleLocaltransitionExternalSuperDoesEntryExitToSub() { + context.register(Config23.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + assertThat(stateMachine, notNullValue()); + TestListener listener = new TestListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + stateMachine.sendEvent("E20"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S21")); + assertThat(listener.entered, containsInAnyOrder("S2", "S21")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleLocaltransitionLocalSuperDoesNotEntryExitToSub() { + context.register(Config23.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + assertThat(stateMachine, notNullValue()); + TestListener listener = new TestListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + stateMachine.sendEvent("E30"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S21")); + assertThat(listener.entered, containsInAnyOrder("S21")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleLocaltransitionExternalToNonInitialSuperDoesEntryExitToSub() { + context.register(Config23.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + assertThat(stateMachine, notNullValue()); + TestListener listener = new TestListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + stateMachine.sendEvent("E21"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S21")); + assertThat(listener.entered, containsInAnyOrder("S2", "S22")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleLocaltransitionLocalToNonInitialSuperDoesNotEntryExitToSub() { + context.register(Config23.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + assertThat(stateMachine, notNullValue()); + TestListener listener = new TestListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + stateMachine.sendEvent("E31"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S21")); + assertThat(listener.entered, containsInAnyOrder("S22")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleLocaltransitionExternalSuperDoesEntryExitToParent() { + context.register(Config23.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + assertThat(stateMachine, notNullValue()); + TestListener listener = new TestListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + stateMachine.sendEvent("E22"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S21")); + assertThat(listener.entered, containsInAnyOrder("S2", "S21")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleLocaltransitionLocalSuperDoesNotEntryExitToParent() { + context.register(Config23.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + assertThat(stateMachine, notNullValue()); + TestListener listener = new TestListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + stateMachine.sendEvent("E32"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S21")); + assertThat(listener.entered, containsInAnyOrder("S21")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleLocaltransitionExternalToNonInitialSuperDoesEntryExitToParent() { + context.register(Config23.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + assertThat(stateMachine, notNullValue()); + TestListener listener = new TestListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + stateMachine.sendEvent("E21"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S21")); + assertThat(listener.entered, containsInAnyOrder("S2", "S22")); + + listener.reset(); + stateMachine.sendEvent("E23"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(2)); + assertThat(listener.entered.size(), is(2)); + assertThat(listener.exited, containsInAnyOrder("S2", "S22")); + assertThat(listener.entered, containsInAnyOrder("S2", "S22")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSimpleLocaltransitionLocalToNonInitialSuperDoesNotEntryExitToParent() { + context.register(Config23.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + assertThat(stateMachine, notNullValue()); + TestListener listener = new TestListener(); + stateMachine.addStateListener(listener); + stateMachine.start(); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21")); + + listener.reset(); + stateMachine.sendEvent("E31"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S21")); + assertThat(listener.entered, containsInAnyOrder("S22")); + + listener.reset(); + stateMachine.sendEvent("E33"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); + assertThat(listener.exited.size(), is(1)); + assertThat(listener.entered.size(), is(1)); + assertThat(listener.exited, containsInAnyOrder("S22")); + assertThat(listener.entered, containsInAnyOrder("S22")); + } + @Configuration @EnableStateMachine public static class Config2 extends StateMachineConfigurerAdapter { @@ -1113,6 +1308,23 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { } } + @Configuration + @EnableStateMachine + public static class Config23 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineModelConfigurer model) throws Exception { + model + .withModel() + .factory(modelFactory()); + } + + @Bean + public StateMachineModelFactory modelFactory() { + return new UmlStateMachineModelFactory("classpath:org/springframework/statemachine/uml/simple-localtransition.uml"); + } + } + public static class LatchAction implements Action { CountDownLatch latch = new CountDownLatch(1); @Override @@ -1163,4 +1375,25 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { return ObjectUtils.nullSafeEquals(match, context.getMessageHeaders().get("junction", String.class)); } } + + private static class TestListener extends StateMachineListenerAdapter { + + final ArrayList entered = new ArrayList<>(); + final ArrayList exited = new ArrayList<>(); + + @Override + public void stateEntered(State state) { + entered.add(state.getId()); + } + + @Override + public void stateExited(State state) { + exited.add(state.getId()); + } + + public void reset() { + entered.clear(); + exited.clear(); + } + } } diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.di b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.di new file mode 100644 index 00000000..bf9abab3 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.di @@ -0,0 +1,2 @@ + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.notation b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.notation new file mode 100644 index 00000000..dd96adf6 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.notation @@ -0,0 +1,288 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.uml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.uml new file mode 100644 index 00000000..a6d4a788 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-localtransition.uml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +