/*
* 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;
}
}
}