/* * Copyright 2016-2017 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.uml.support; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.uml2.uml.Activity; import org.eclipse.uml2.uml.ConnectionPointReference; import org.eclipse.uml2.uml.Constraint; import org.eclipse.uml2.uml.Event; import org.eclipse.uml2.uml.Model; import org.eclipse.uml2.uml.OpaqueBehavior; import org.eclipse.uml2.uml.OpaqueExpression; import org.eclipse.uml2.uml.PackageableElement; import org.eclipse.uml2.uml.Pseudostate; import org.eclipse.uml2.uml.PseudostateKind; import org.eclipse.uml2.uml.Region; import org.eclipse.uml2.uml.Signal; import org.eclipse.uml2.uml.SignalEvent; import org.eclipse.uml2.uml.State; import org.eclipse.uml2.uml.StateMachine; import org.eclipse.uml2.uml.TimeEvent; import org.eclipse.uml2.uml.Transition; import org.eclipse.uml2.uml.Trigger; import org.eclipse.uml2.uml.UMLPackage; import org.eclipse.uml2.uml.Vertex; import org.springframework.expression.spel.SpelCompilerMode; import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.action.SpelExpressionAction; import org.springframework.statemachine.config.model.ChoiceData; import org.springframework.statemachine.config.model.EntryData; import org.springframework.statemachine.config.model.ExitData; import org.springframework.statemachine.config.model.HistoryData; import org.springframework.statemachine.config.model.JunctionData; import org.springframework.statemachine.config.model.StateData; import org.springframework.statemachine.config.model.StateMachineComponentResolver; import org.springframework.statemachine.config.model.StatesData; import org.springframework.statemachine.config.model.TransitionData; import org.springframework.statemachine.config.model.TransitionsData; import org.springframework.statemachine.guard.Guard; import org.springframework.statemachine.guard.SpelExpressionGuard; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Model parser which constructs states and transitions data out from * an uml model. * * @author Janne Valkealahti */ public class UmlModelParser { public final static String LANGUAGE_BEAN = "bean"; public final static String LANGUAGE_SPEL = "spel"; private final Model model; private final StateMachineComponentResolver<String, String> resolver; private final Collection<StateData<String, String>> stateDatas = new ArrayList<StateData<String, String>>(); private final Collection<TransitionData<String, String>> transitionDatas = new ArrayList<TransitionData<String, String>>(); private final Collection<EntryData<String, String>> entrys = new ArrayList<EntryData<String, String>>(); private final Collection<ExitData<String, String>> exits = new ArrayList<ExitData<String, String>>(); private final Collection<HistoryData<String, String>> historys = new ArrayList<HistoryData<String, String>>(); private final Map<String, LinkedList<ChoiceData<String, String>>> choices = new HashMap<String, LinkedList<ChoiceData<String,String>>>(); private final Map<String, LinkedList<JunctionData<String, String>>> junctions = new HashMap<String, LinkedList<JunctionData<String,String>>>(); private final Map<String, List<String>> forks = new HashMap<String, List<String>>(); private final Map<String, List<String>> joins = new HashMap<String, List<String>>(); /** * Instantiates a new uml model parser. * * @param model the model * @param resolver the resolver */ public UmlModelParser(Model model, StateMachineComponentResolver<String, String> resolver) { Assert.notNull(model, "Model must be set"); Assert.notNull(resolver, "Resolver must be set"); this.model = model; this.resolver = resolver; } /** * Parses the model. * * @return the data holder for states and transitions */ public DataHolder parseModel() { EList<PackageableElement> packagedElements = model.getPackagedElements(); // expect root machine to be a one having no machines in a submachineState field. StateMachine stateMachine = null; Collection<StateMachine> 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<State> submachineRefs = machine.getSubmachineStates(); if (submachineRefs.size() == 0) { stateMachine = machine; } handleStateMachine(machine); } // 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<String, List<ChoiceData<String, String>>> choicesCopy = new HashMap<String, List<ChoiceData<String, String>>>(); choicesCopy.putAll(choices); HashMap<String, List<JunctionData<String, String>>> junctionsCopy = new HashMap<String, List<JunctionData<String, String>>>(); junctionsCopy.putAll(junctions); return new DataHolder(new StatesData<>(stateDatas), new TransitionsData<String, String>(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<State> submachineStates = ((StateMachine)region.getOwner()).getSubmachineStates(); if (submachineStates.size() == 1) { parent = submachineStates.get(0).getName(); } } if (state.getOwner() instanceof Region) { regionId = ((Region)state.getOwner()).getName(); } boolean isInitialState = UmlUtils.isInitialState(state); StateData<String, String> stateData = handleActions( new StateData<String, String>(parent, regionId, state.getName(), isInitialState), state); if (isInitialState) { // set possible initial transition stateData.setInitialAction(resolveInitialTransitionAction(state)); } stateData.setDeferred(UmlUtils.resolveDererredEvents(state)); if (UmlUtils.isFinalState(state)) { stateData.setEnd(true); } stateDatas.add(stateData); // add states via entry/exit reference points for (ConnectionPointReference cpr : state.getConnections()) { if (cpr.getEntries() != null) { for (Pseudostate cp : cpr.getEntries()) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, cp.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.ENTRY); stateDatas.add(cpStateData); } } if (cpr.getExits() != null) { for (Pseudostate cp : cpr.getExits()) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, cp.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.EXIT); stateDatas.add(cpStateData); } } } // add states via entry/exit points for (Pseudostate cp : state.getConnectionPoints()) { PseudoStateKind kind = null; if (cp.getKind() == PseudostateKind.ENTRY_POINT_LITERAL) { kind = PseudoStateKind.ENTRY; } else if (cp.getKind() == PseudostateKind.EXIT_POINT_LITERAL) { kind = PseudoStateKind.EXIT; } if (kind != null) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, cp.getName(), false); cpStateData.setPseudoStateKind(kind); stateDatas.add(cpStateData); } } // do recursive handling of regions for (Region sub : state.getRegions()) { handleRegion(sub); } } // pseudostates like choice, etc if (vertex instanceof Pseudostate) { Pseudostate state = (Pseudostate)vertex; String parent = null; String regionId = null; if (state.getContainer().getOwner() instanceof State) { parent = ((State)state.getContainer().getOwner()).getName(); } if (state.getOwner() instanceof Region) { regionId = ((Region)state.getOwner()).getName(); } if (state.getKind() == PseudostateKind.CHOICE_LITERAL) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.CHOICE); stateDatas.add(cpStateData); } else if (state.getKind() == PseudostateKind.JUNCTION_LITERAL) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.JUNCTION); stateDatas.add(cpStateData); } else if (state.getKind() == PseudostateKind.FORK_LITERAL) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.FORK); stateDatas.add(cpStateData); } else if (state.getKind() == PseudostateKind.JOIN_LITERAL) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.JOIN); stateDatas.add(cpStateData); } else if (state.getKind() == PseudostateKind.SHALLOW_HISTORY_LITERAL) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.HISTORY_SHALLOW); stateDatas.add(cpStateData); } else if (state.getKind() == PseudostateKind.DEEP_HISTORY_LITERAL) { StateData<String, String> cpStateData = new StateData<>(parent, regionId, state.getName(), false); cpStateData.setPseudoStateKind(PseudoStateKind.HISTORY_DEEP); stateDatas.add(cpStateData); } } } // build transitions for (Transition transition : region.getTransitions()) { // for entry/exit points we need to create these outside // of triggers as link from point to a state is most likely // just a link and don't have any triggers. // little unclear for now if link from points to a state should // have trigger? // anyway, we need to add entrys and exits to a model if (transition.getSource() instanceof ConnectionPointReference) { // support ref points if only one is defined as for some // reason uml can define multiple ones which is not // realistic with state machines EList<Pseudostate> cprentries = ((ConnectionPointReference)transition.getSource()).getEntries(); if (cprentries != null && cprentries.size() == 1 && cprentries.get(0).getKind() == PseudostateKind.ENTRY_POINT_LITERAL) { entrys.add(new EntryData<String, String>(cprentries.get(0).getName(), transition.getTarget().getName())); } EList<Pseudostate> cprexits = ((ConnectionPointReference)transition.getSource()).getExits(); if (cprexits != null && cprexits.size() == 1 && cprexits.get(0).getKind() == PseudostateKind.EXIT_POINT_LITERAL) { exits.add(new ExitData<String, String>(cprexits.get(0).getName(), transition.getTarget().getName())); } } if (transition.getSource() instanceof Pseudostate) { if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.ENTRY_POINT_LITERAL) { entrys.add(new EntryData<String, String>(transition.getSource().getName(), transition.getTarget().getName())); } else if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.EXIT_POINT_LITERAL) { exits.add(new ExitData<String, String>(transition.getSource().getName(), transition.getTarget().getName())); } else if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.CHOICE_LITERAL) { LinkedList<ChoiceData<String, String>> list = choices.get(transition.getSource().getName()); if (list == null) { list = new LinkedList<ChoiceData<String, String>>(); choices.put(transition.getSource().getName(), list); } Guard<String, String> guard = resolveGuard(transition); Collection<Action<String,String>> actions = UmlUtils.resolveTransitionActions(transition, resolver); // we want null guards to be at the end if (guard == null) { list.addLast(new ChoiceData<String, String>(transition.getSource().getName(), transition.getTarget().getName(), guard, actions)); } else { list.addFirst(new ChoiceData<String, String>(transition.getSource().getName(), transition.getTarget().getName(), guard, actions)); } } else if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.JUNCTION_LITERAL) { LinkedList<JunctionData<String, String>> list = junctions.get(transition.getSource().getName()); if (list == null) { list = new LinkedList<JunctionData<String, String>>(); junctions.put(transition.getSource().getName(), list); } Guard<String, String> guard = resolveGuard(transition); Collection<Action<String,String>> actions = UmlUtils.resolveTransitionActions(transition, resolver); // we want null guards to be at the end if (guard == null) { list.addLast(new JunctionData<String, String>(transition.getSource().getName(), transition.getTarget().getName(), guard, actions)); } else { list.addFirst(new JunctionData<String, String>(transition.getSource().getName(), transition.getTarget().getName(), guard, actions)); } } else if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.FORK_LITERAL) { List<String> list = forks.get(transition.getSource().getName()); if (list == null) { list = new ArrayList<String>(); forks.put(transition.getSource().getName(), list); } list.add(transition.getTarget().getName()); } else if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.SHALLOW_HISTORY_LITERAL) { historys.add(new HistoryData<String, String>(transition.getSource().getName(), transition.getTarget().getName())); } else if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.DEEP_HISTORY_LITERAL) { historys.add(new HistoryData<String, String>(transition.getSource().getName(), transition.getTarget().getName())); } } if (transition.getTarget() instanceof Pseudostate) { if (((Pseudostate)transition.getTarget()).getKind() == PseudostateKind.JOIN_LITERAL) { List<String> list = joins.get(transition.getTarget().getName()); if (list == null) { list = new ArrayList<String>(); joins.put(transition.getTarget().getName(), list); } list.add(transition.getSource().getName()); } } // go through all triggers and create transition // from signals, or transitions from timers for (Trigger trigger : transition.getTriggers()) { Guard<String, String> guard = resolveGuard(transition); Event event = trigger.getEvent(); if (event instanceof SignalEvent) { Signal signal = ((SignalEvent)event).getSignal(); if (signal != null) { // special case for ref point if (transition.getTarget() instanceof ConnectionPointReference) { EList<Pseudostate> cprentries = ((ConnectionPointReference)transition.getTarget()).getEntries(); if (cprentries != null && cprentries.size() == 1) { transitionDatas.add(new TransitionData<String, String>(transition.getSource().getName(), cprentries.get(0).getName(), signal.getName(), UmlUtils.resolveTransitionActions(transition, resolver), guard, UmlUtils.mapUmlTransitionType(transition))); } } else { transitionDatas.add(new TransitionData<String, String>(transition.getSource().getName(), transition.getTarget().getName(), signal.getName(), UmlUtils.resolveTransitionActions(transition, resolver), guard, UmlUtils.mapUmlTransitionType(transition))); } } } else if (event instanceof TimeEvent) { TimeEvent timeEvent = (TimeEvent)event; Long period = getTimePeriod(timeEvent); if (period != null) { Integer count = null; if (timeEvent.isRelative()) { count = 1; } transitionDatas.add(new TransitionData<String, String>(transition.getSource().getName(), transition.getTarget().getName(), period, count, UmlUtils.resolveTransitionActions(transition, resolver), guard, UmlUtils.mapUmlTransitionType(transition))); } } } // create anonymous transition if needed if (shouldCreateAnonymousTransition(transition)) { transitionDatas.add(new TransitionData<String, String>(transition.getSource().getName(), transition.getTarget().getName(), null, UmlUtils.resolveTransitionActions(transition, resolver), resolveGuard(transition), UmlUtils.mapUmlTransitionType(transition))); } } } private Guard<String, String> resolveGuard(Transition transition) { Guard<String, String> guard = null; for (Constraint c : transition.getOwnedRules()) { if (c.getSpecification() instanceof OpaqueExpression) { String beanId = UmlUtils.resolveBodyByLanguage(LANGUAGE_BEAN, (OpaqueExpression)c.getSpecification()); if (StringUtils.hasText(beanId)) { guard = resolver.resolveGuard(beanId); } else { String expression = UmlUtils.resolveBodyByLanguage(LANGUAGE_SPEL, (OpaqueExpression)c.getSpecification()); if (StringUtils.hasText(expression)) { SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); guard = new SpelExpressionGuard<String, String>(parser.parseExpression(expression)); } } } } return guard; } private Long getTimePeriod(TimeEvent event) { try { return Long.valueOf(event.getWhen().getExpr().integerValue()); } catch (Exception e) { return null; } } private Action<String, String> resolveInitialTransitionAction(State state) { Transition transition = UmlUtils.resolveInitialTransition(state); if (transition != null) { return UmlUtils.resolveTransitionAction(transition, resolver); } else { return null; } } private boolean shouldCreateAnonymousTransition(Transition transition) { if (transition.getSource() == null || transition.getTarget() == null) { // nothing to do as would cause NPE later return false; } if (!transition.getTriggers().isEmpty()) { return false; } if (!StringUtils.hasText(transition.getSource().getName())) { return false; } if (!StringUtils.hasText(transition.getTarget().getName())) { return false; } if (transition.getSource() instanceof Pseudostate) { if (((Pseudostate)transition.getSource()).getKind() == PseudostateKind.FORK_LITERAL) { return false; } } if (transition.getTarget() instanceof Pseudostate) { if (((Pseudostate)transition.getTarget()).getKind() == PseudostateKind.JOIN_LITERAL) { return false; } } return true; } private StateData<String, String> handleActions(StateData<String, String> stateData, State state) { if (state.getEntry() instanceof OpaqueBehavior) { String beanId = UmlUtils.resolveBodyByLanguage(LANGUAGE_BEAN, (OpaqueBehavior)state.getEntry()); if (StringUtils.hasText(beanId)) { Action<String, String> bean = resolver.resolveAction(beanId); if (bean != null) { ArrayList<Action<String, String>> entrys = new ArrayList<Action<String, String>>(); entrys.add(bean); stateData.setEntryActions(entrys); } } else { String expression = UmlUtils.resolveBodyByLanguage(LANGUAGE_SPEL, (OpaqueBehavior)state.getEntry()); if (StringUtils.hasText(expression)) { SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); ArrayList<Action<String, String>> entrys = new ArrayList<Action<String, String>>(); entrys.add(new SpelExpressionAction<String, String>(parser.parseExpression(expression))); stateData.setEntryActions(entrys); } } } if (state.getExit() instanceof OpaqueBehavior) { String beanId = UmlUtils.resolveBodyByLanguage(LANGUAGE_BEAN, (OpaqueBehavior)state.getExit()); if (StringUtils.hasText(beanId)) { Action<String, String> bean = resolver.resolveAction(beanId); if (bean != null) { ArrayList<Action<String, String>> exits = new ArrayList<Action<String, String>>(); exits.add(bean); stateData.setExitActions(exits); } } else { String expression = UmlUtils.resolveBodyByLanguage(LANGUAGE_SPEL, (OpaqueBehavior)state.getExit()); if (StringUtils.hasText(expression)) { SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); ArrayList<Action<String, String>> exits = new ArrayList<Action<String, String>>(); exits.add(new SpelExpressionAction<String, String>(parser.parseExpression(expression))); stateData.setExitActions(exits); } } } if (state.getDoActivity() instanceof OpaqueBehavior) { String beanId = UmlUtils.resolveBodyByLanguage(LANGUAGE_BEAN, (OpaqueBehavior)state.getDoActivity()); if (StringUtils.hasText(beanId)) { Action<String, String> bean = resolver.resolveAction(beanId); if (bean != null) { ArrayList<Action<String, String>> stateActions = new ArrayList<Action<String, String>>(); stateActions.add(bean); stateData.setStateActions(stateActions); } } else { String expression = UmlUtils.resolveBodyByLanguage(LANGUAGE_SPEL, (OpaqueBehavior)state.getDoActivity()); if (StringUtils.hasText(expression)) { SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); ArrayList<Action<String, String>> stateActions = new ArrayList<Action<String, String>>(); stateActions.add(new SpelExpressionAction<String, String>(parser.parseExpression(expression))); stateData.setExitActions(stateActions); } } } if (state.getEntry() instanceof Activity) { String beanId = ((Activity)state.getEntry()).getName(); Action<String, String> bean = resolver.resolveAction(beanId); if (bean != null) { ArrayList<Action<String, String>> entrys = new ArrayList<Action<String, String>>(); entrys.add(bean); stateData.setEntryActions(entrys); } } if (state.getExit() instanceof Activity) { String beanId = ((Activity)state.getExit()).getName(); Action<String, String> bean = resolver.resolveAction(beanId); if (bean != null) { ArrayList<Action<String, String>> exits = new ArrayList<Action<String, String>>(); exits.add(bean); stateData.setExitActions(exits); } } if (state.getDoActivity() instanceof Activity) { String beanId = ((Activity)state.getDoActivity()).getName(); Action<String, String> bean = resolver.resolveAction(beanId); if (bean != null) { ArrayList<Action<String, String>> stateActions = new ArrayList<Action<String, String>>(); stateActions.add(bean); stateData.setStateActions(stateActions); } } return stateData; } /** * Holder object for results returned from uml parser. */ public class DataHolder { private final StatesData<String, String> statesData; private final TransitionsData<String, String> transitionsData; /** * Instantiates a new data holder. * * @param statesData the states data * @param transitionsData the transitions data */ public DataHolder(StatesData<String, String> statesData, TransitionsData<String, String> transitionsData) { this.statesData = statesData; this.transitionsData = transitionsData; } /** * Gets the states data. * * @return the states data */ public StatesData<String, String> getStatesData() { return statesData; } /** * Gets the transitions data. * * @return the transitions data */ public TransitionsData<String, String> getTransitionsData() { return transitionsData; } } }