From c387979d751fae6be0e602fa6060157b11358502 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Sat, 13 Aug 2016 07:19:21 +0100 Subject: [PATCH] Support for UML Sub State Machines - Enhance config model so that a state can be configured as a submachine or a factory. - With uml it's possible to put multiple machines into one xml(one needs to be a root machine) and in a same way, define state as a separate machine. - JavaConfig vs. uml config differs in a bit where latter is passing raw model data into pojo config and JavaConfig either full machine or machine factory. - Fixes #121 --- .../config/AbstractStateMachineFactory.java | 33 ++- .../configurers/DefaultStateConfigurer.java | 20 ++ .../config/configurers/StateConfigurer.java | 22 ++ .../statemachine/config/model/StateData.java | 67 +++++ .../config/model/StateMachineModelTests.java | 45 +++ .../state/SubmachineRefEnumTests.java | 204 +++++++++++++ .../state/SubmachineRefTests.java | 190 ++++++++++++ .../uml/support/UmlModelParser.java | 35 ++- .../uml/UmlStateMachineModelFactoryTests.java | 33 +++ .../statemachine/uml/simple-submachineref.di | 2 + .../uml/simple-submachineref.notation | 272 ++++++++++++++++++ .../statemachine/uml/simple-submachineref.uml | 41 +++ 12 files changed, 946 insertions(+), 18 deletions(-) create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineRefEnumTests.java create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineRefTests.java create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.di create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.notation create mode 100644 spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.uml 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 78b0c53f..cac6636e 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 @@ -430,6 +430,14 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS for (StateData stateData : stateDatas) { StateMachine stateMachine = machineMap.get(stateData.getState()); + if (stateMachine == null) { + // get a submachine from state data if we didn't have + // it already. stays null if we don't have one. + stateMachine = stateData.getSubmachine(); + if (stateMachine == null && stateData.getSubmachineFactory() != null) { + stateMachine = stateData.getSubmachineFactory().getStateMachine(machineId); + } + } state = stateMap.get(stateData.getState()); if (state != null) { states.add(state); @@ -732,23 +740,24 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS private Iterator>> buildStateDataIterator() { Tree> tree = new Tree>(); - - for (StateData stateData : stateMachineModel.getStatesData().getStateData()) { - Object id = stateData.getState(); - Object parent = stateData.getParent(); - tree.add(stateData, id, parent); - } - - TreeTraverser>> traverser = new TreeTraverser>>() { + treeAdd(tree, stateMachineModel.getStatesData().getStateData()); + return new TreeTraverser>>() { @Override public Iterable>> children(Node> root) { return root.getChildren(); } - }; + }.postOrderTraversal(tree.getRoot()).iterator(); + } - Iterable>> postOrderTraversal = traverser.postOrderTraversal(tree.getRoot()); - Iterator>> iterator = postOrderTraversal.iterator(); - return iterator; + private void treeAdd(Tree> tree, Collection> stateDatas) { + // recursive call due to possible submachine data ref + if (stateDatas == null) { + return; + } + for (StateData stateData : stateDatas) { + tree.add(stateData, stateData.getState(), stateData.getParent()); + treeAdd(tree, stateData.getSubmachineStateData()); + } } protected abstract RegionState buildRegionStateInternal(S id, Collection> regions, Collection deferred, diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java index 6267b03f..ca136bc2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultStateConfigurer.java @@ -23,7 +23,9 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.config.StateMachineFactory; import org.springframework.statemachine.config.builders.StateMachineStateBuilder; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter; @@ -57,6 +59,8 @@ public class DefaultStateConfigurer private final Collection joins = new ArrayList(); private final Collection exits = new ArrayList(); private final Collection entrys = new ArrayList(); + private final Map> submachines = new HashMap<>(); + private final Map> submachinefactories = new HashMap<>(); @Override public void configure(StateMachineStateBuilder builder) throws Exception { @@ -93,6 +97,8 @@ public class DefaultStateConfigurer s.setPseudoStateKind(PseudoStateKind.HISTORY_DEEP); } } + s.setSubmachine(submachines.get(s.getState())); + s.setSubmachineFactory(submachinefactories.get(s.getState())); } builder.addStateData(stateDatas); } @@ -128,6 +134,20 @@ public class DefaultStateConfigurer return state(state, (E[])null); } + @Override + public StateConfigurer state(S state, StateMachine stateMachine) { + state(state); + submachines.put(state, stateMachine); + return this; + } + + @Override + public StateConfigurer state(S state, StateMachineFactory stateMachineFactory) { + state(state); + submachinefactories.put(state, stateMachineFactory); + return this; + } + @Override public StateConfigurer state(S state, Collection> entryActions, Collection> exitActions) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java index 5d1b57fd..a417e862 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/StateConfigurer.java @@ -18,7 +18,9 @@ package org.springframework.statemachine.config.configurers; import java.util.Collection; import java.util.Set; +import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; +import org.springframework.statemachine.config.StateMachineFactory; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder; import org.springframework.statemachine.state.State; @@ -69,6 +71,26 @@ public interface StateConfigurer extends */ StateConfigurer state(S state); + /** + * Specify a state {@code S} and its relation with a given + * machine as substate machine. + * + * @param state the state + * @param stateMachine the submachine + * @return configurer for chaining + */ + StateConfigurer state(S state, StateMachine stateMachine); + + /** + * Specify a state {@code S} and its relation with a given + * machine as substate machine factory. + * + * @param state the state + * @param stateMachineFactory the submachine factory + * @return configurer for chaining + */ + StateConfigurer state(S state, StateMachineFactory stateMachineFactory); + /** * Specify a state {@code S} with entry and exit {@link Action}s. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/StateData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/StateData.java index fc953c8e..009a61fa 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/StateData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/StateData.java @@ -17,6 +17,7 @@ package org.springframework.statemachine.config.model; import java.util.Collection; +import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.StateMachineFactory; import org.springframework.statemachine.state.PseudoStateKind; @@ -37,6 +38,9 @@ public class StateData { private Object parent; private Object region; private S state; + private Collection> submachineStateData; + private StateMachine submachine; + private StateMachineFactory submachineFactory; private Collection deferred; private Collection> entryActions; private Collection> exitActions; @@ -122,6 +126,69 @@ public class StateData { return state; } + /** + * Gets the submachine state data. + * + * @return the submachine state data + */ + public Collection> getSubmachineStateData() { + return submachineStateData; + } + + /** + * Sets the submachine state data. + * + * @param submachineStateData the submachine state data + */ + public void setSubmachineStateData(Collection> submachineStateData) { + this.submachineStateData = submachineStateData; + } + + /** + * Gets the submachine. + * + * @return the submachine + */ + public StateMachine getSubmachine() { + return submachine; + } + + /** + * Sets the submachine. + * + * @param submachine the submachine + */ + public void setSubmachine(StateMachine submachine) { + this.submachine = submachine; + } + + /** + * Gets the submachine factory. + * + * @return the submachine factory + */ + public StateMachineFactory getSubmachineFactory() { + return submachineFactory; + } + + /** + * Sets the submachine factory. + * + * @param submachineFactory the submachine factory + */ + public void setSubmachineFactory(StateMachineFactory submachineFactory) { + this.submachineFactory = submachineFactory; + } + + /** + * Sets the submachine factory. + * + * @param submachineFactory the submachine factory + */ + public void setSubmachine(StateMachineFactory submachineFactory) { + this.submachineFactory = submachineFactory; + } + /** * Gets the deferred. * diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java index ca752077..080195b9 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/config/model/StateMachineModelTests.java @@ -16,6 +16,7 @@ package org.springframework.statemachine.config.model; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertThat; import java.util.ArrayList; @@ -111,4 +112,48 @@ public class StateMachineModelTests { stateMachine.sendEvent("E1"); assertThat(stateMachine.getState().getIds(), contains("S2")); } + + @Test + public void testSubmachineRefConfig() { + // *S1 S2 + // / \ + // *S20 S21 + // / \ + // *S30 S31 + ConfigurationData configurationData = new ConfigurationData<>(); + + Collection> stateData2 = new ArrayList<>(); + stateData2.add(new StateData("S2", null, "S20", true)); + stateData2.add(new StateData("S2", null, "S21", false)); + stateData2.add(new StateData("S21", null, "S30", true)); + stateData2.add(new StateData("S21", null, "S31", false)); + + Collection> stateData1 = new ArrayList<>(); + stateData1.add(new StateData("S1", true)); + StateData stateDataS2 = new StateData("S2"); + stateDataS2.setSubmachineStateData(stateData2); + stateData1.add(stateDataS2); + StatesData statesData = new StatesData<>(stateData1); + + Collection> transitionData = new ArrayList<>(); + transitionData.add(new TransitionData("S1", "S2", "E1")); + transitionData.add(new TransitionData("S20", "S21", "E2")); + transitionData.add(new TransitionData("S30", "S31", "E3")); + TransitionsData transitionsData = new TransitionsData<>(transitionData); + + StateMachineModel stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData, transitionsData); + + ObjectStateMachineFactory factory = new ObjectStateMachineFactory<>(stateMachineModel); + + StateMachine stateMachine = factory.getStateMachine(); + stateMachine.start(); + + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S1")); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S20")); + stateMachine.sendEvent("E2"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21", "S30")); + stateMachine.sendEvent("E3"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21", "S31")); + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineRefEnumTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineRefEnumTests.java new file mode 100644 index 00000000..61565c22 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineRefEnumTests.java @@ -0,0 +1,204 @@ +/* + * 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.state; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +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.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +/** + * Tests for submachine references. + * + * @author Janne Valkealahti + * + */ +public class SubmachineRefEnumTests extends AbstractStateMachineTests { + + @Test + @SuppressWarnings("unchecked") + public void testSubmachineRef() throws Exception { + context.register(Config2.class, Config1.class); + context.refresh(); + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S1)); + machine.sendEvent(TestEvents.E1); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S20)); + machine.sendEvent(TestEvents.E2); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S21, TestStates.S30)); + machine.sendEvent(TestEvents.E3); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S21, TestStates.S31)); + } + + @Test + @SuppressWarnings("unchecked") + public void testSubmachineRefDifferentTypes() throws Exception { + context.register(Config4.class, Config3.class); + context.refresh(); + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + assertThat(machine.getState().getIds(), containsInAnyOrder(States1.S1)); + machine.sendEvent(Events1.E1); + assertThat(machine.getState().getIds(), containsInAnyOrder(States1.S2, States2.S20)); + machine.sendEvent(Events2.E2); + assertThat(machine.getState().getIds(), containsInAnyOrder(States1.S2, States2.S21, States2.S30)); + machine.sendEvent(Events2.E3); + assertThat(machine.getState().getIds(), containsInAnyOrder(States1.S2, States2.S21, States2.S31)); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Autowired + @Qualifier("subStateMachine") + private StateMachine subStateMachine; + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2, subStateMachine); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1).target(TestStates.S2).event(TestEvents.E1); + } + + } + + @Configuration + @EnableStateMachine(name = "subStateMachine") + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S20) + .state(TestStates.S21) + .and() + .withStates() + .parent(TestStates.S21) + .initial(TestStates.S30) + .state(TestStates.S31); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S20).target(TestStates.S21).event(TestEvents.E2).and() + .withExternal() + .source(TestStates.S30).target(TestStates.S31).event(TestEvents.E3); + } + + } + + @Configuration + @EnableStateMachine + static class Config3 extends StateMachineConfigurerAdapter { + + @Autowired + @Qualifier("subStateMachine") + private StateMachine subStateMachine; + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(States1.S1) + .state(States1.S2, subStateMachine); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(States1.S1).target(States1.S2).event(Events1.E1); + } + + } + + @Configuration + @EnableStateMachine(name = "subStateMachine") + static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(States2.S20) + .state(States2.S21) + .and() + .withStates() + .parent(States2.S21) + .initial(States2.S30) + .state(States2.S31); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(States2.S20).target(States2.S21).event(Events2.E2).and() + .withExternal() + .source(States2.S30).target(States2.S31).event(Events2.E3); + } + + } + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + enum States1 { + S1, S2; + } + + enum Events1 { + E1 + } + + enum States2 { + S20, S21, S30, S31; + } + + enum Events2 { + E2, E3; + } +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineRefTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineRefTests.java new file mode 100644 index 00000000..9921b797 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineRefTests.java @@ -0,0 +1,190 @@ +/* + * 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.state; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +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.EnableStateMachineFactory; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +/** + * Tests for submachine references. + * + * @author Janne Valkealahti + * + */ +public class SubmachineRefTests extends AbstractStateMachineTests { + + @Test + @SuppressWarnings("unchecked") + public void testSubmachineRef() throws Exception { + context.register(Config2.class, Config1.class); + context.refresh(); + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + assertThat(machine, notNullValue()); + machine.start(); + assertThat(machine.getState().getIds(), containsInAnyOrder("S1")); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S20")); + machine.sendEvent("E2"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21", "S30")); + machine.sendEvent("E3"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21", "S31")); + } + + @Test + @SuppressWarnings("unchecked") + public void testSubmachineRefWithFactory() throws Exception { + context.register(Config4.class, Config3.class); + context.refresh(); + StateMachineFactory factory = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + StateMachine machine = factory.getStateMachine(); + assertThat(machine, notNullValue()); + machine.start(); + assertThat(machine.getState().getIds(), containsInAnyOrder("S1")); + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S20")); + machine.sendEvent("E2"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21", "S30")); + machine.sendEvent("E3"); + assertThat(machine.getState().getIds(), containsInAnyOrder("S2", "S21", "S31")); + } + + @Configuration + @EnableStateMachine + static class Config1 extends StateMachineConfigurerAdapter { + + @Autowired + @Qualifier("subStateMachine") + private StateMachine subStateMachine; + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2", subStateMachine); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1").target("S2").event("E1"); + } + + } + + @Configuration + @EnableStateMachine(name = "subStateMachine") + static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S20") + .state("S21") + .and() + .withStates() + .parent("S21") + .initial("S30") + .state("S31"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S20").target("S21").event("E2").and() + .withExternal() + .source("S30").target("S31").event("E3"); + } + + } + + @Configuration + @EnableStateMachineFactory + static class Config3 extends StateMachineConfigurerAdapter { + + @Autowired + @Qualifier("subStateMachineFactory") + private StateMachineFactory subStateMachineFactory; + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2", subStateMachineFactory); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1").target("S2").event("E1"); + } + + } + + @Configuration + @EnableStateMachineFactory(name = "subStateMachineFactory") + static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S20") + .state("S21") + .and() + .withStates() + .parent("S21") + .initial("S30") + .state("S31"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S20").target("S21").event("E2").and() + .withExternal() + .source("S30").target("S31").event("E3"); + } + + } + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } +} diff --git a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java index 2d69fc39..372ba0a1 100644 --- a/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java +++ b/spring-statemachine-uml/src/main/java/org/springframework/statemachine/uml/support/UmlModelParser.java @@ -106,15 +106,24 @@ public class UmlModelParser { */ public DataHolder parseModel() { EList packagedElements = model.getPackagedElements(); - // expect model having exactly one machine - StateMachine stateMachine = (StateMachine) EcoreUtil.getObjectByType(packagedElements, UMLPackage.Literals.STATE_MACHINE); - if (stateMachine == null) { - throw new IllegalArgumentException("Can't find statemachine from model"); + + // expect root machine to be a one having no machines in a submachineState field. + StateMachine stateMachine = null; + Collection stateMachines = EcoreUtil.getObjectsByType(packagedElements, UMLPackage.Literals.STATE_MACHINE); + for (StateMachine machine : stateMachines) { + // multiple substates can point to same machine, thus it's a back reference list + EList submachineRefs = machine.getSubmachineStates(); + if (submachineRefs.size() == 0) { + stateMachine = machine; + } + handleStateMachine(machine); } - for (Region region : stateMachine.getRegions()) { - handleRegion(region); + // all machines are iterated so we only do sanity check here for a root machine + if (stateMachine == null) { + throw new IllegalArgumentException("Can't find root statemachine from model"); } + // LinkedList can be passed due to generics, need to copy HashMap>> choicesCopy = new HashMap>>(); choicesCopy.putAll(choices); @@ -124,18 +133,32 @@ public class UmlModelParser { new TransitionsData(transitionDatas, choicesCopy, junctionsCopy, forks, joins, entrys, exits, historys)); } + private void handleStateMachine(StateMachine stateMachine) { + for (Region region : stateMachine.getRegions()) { + handleRegion(region); + } + } + private void handleRegion(Region region) { // build states for (Vertex vertex : region.getSubvertices()) { // normal states if (vertex instanceof State) { State state = (State)vertex; + // find parent state if submachine state, root states have null parent String parent = null; String regionId = null; if (state.getContainer().getOwner() instanceof State) { parent = ((State)state.getContainer().getOwner()).getName(); } + // if parent is unknown, check if it's a ref where parent is then that + if (parent == null && region.getOwner() instanceof StateMachine) { + EList submachineStates = ((StateMachine)region.getOwner()).getSubmachineStates(); + if (submachineStates.size() == 1) { + parent = submachineStates.get(0).getName(); + } + } if (state.getOwner() instanceof Region) { regionId = ((Region)state.getOwner()).getName(); } 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 12464673..4f4c10b5 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 @@ -642,6 +642,22 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { context.refresh(); } + @Test + @SuppressWarnings("unchecked") + public void testSimpleSubmachineRef() throws Exception { + context.register(Config21.class); + context.refresh(); + StateMachine stateMachine = context.getBean(StateMachine.class); + stateMachine.start(); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S1")); + stateMachine.sendEvent("E1"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S20")); + stateMachine.sendEvent("E2"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21", "S30")); + stateMachine.sendEvent("E3"); + assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S21", "S31")); + } + @Configuration @EnableStateMachine public static class Config2 extends StateMachineConfigurerAdapter { @@ -1035,6 +1051,23 @@ public class UmlStateMachineModelFactoryTests extends AbstractUmlTests { } } + @Configuration + @EnableStateMachine + public static class Config21 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-submachineref.uml"); + } + } + public static class LatchAction implements Action { CountDownLatch latch = new CountDownLatch(1); @Override diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.di b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.di new file mode 100644 index 00000000..bf9abab3 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.di @@ -0,0 +1,2 @@ + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.notation b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.notation new file mode 100644 index 00000000..c6e0cf6c --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.notation @@ -0,0 +1,272 @@ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.uml b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.uml new file mode 100644 index 00000000..deb9d0d5 --- /dev/null +++ b/spring-statemachine-uml/src/test/resources/org/springframework/statemachine/uml/simple-submachineref.uml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +