/** * Copyright (c) 2012-2016 committers of YAKINDU and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * committers of YAKINDU - initial API and implementation */ package org.yakindu.sct.model.sgraph.validation; import static org.yakindu.sct.model.sgraph.util.SGgraphUtil.areOrthogonal; import static org.yakindu.sct.model.sgraph.util.SGgraphUtil.collectAncestors; import static org.yakindu.sct.model.sgraph.util.SGgraphUtil.commonAncestor; import static org.yakindu.sct.model.sgraph.util.SGgraphUtil.findCommonAncestor; import static org.yakindu.sct.model.sgraph.util.SGgraphUtil.sources; import static org.yakindu.sct.model.sgraph.util.SGgraphUtil.targets; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.eclipse.emf.common.util.TreeIterator; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.validation.AbstractDeclarativeValidator; import org.eclipse.xtext.validation.Check; import org.eclipse.xtext.validation.CheckType; import org.eclipse.xtext.validation.EValidatorRegistrar; import org.yakindu.base.base.BasePackage; import org.yakindu.sct.model.sgraph.Choice; import org.yakindu.sct.model.sgraph.CompositeElement; import org.yakindu.sct.model.sgraph.Entry; import org.yakindu.sct.model.sgraph.EntryKind; import org.yakindu.sct.model.sgraph.Exit; import org.yakindu.sct.model.sgraph.FinalState; import org.yakindu.sct.model.sgraph.Region; import org.yakindu.sct.model.sgraph.Statechart; import org.yakindu.sct.model.sgraph.Synchronization; import org.yakindu.sct.model.sgraph.Transition; import org.yakindu.sct.model.sgraph.Vertex; import com.google.inject.Inject; /** * This validator is intended to be used by a compositeValidator (See * {@link org.eclipse.xtext.validation.ComposedChecks}) of another language * specific validator. It does not register itself as an EValidator. * * This validator checks for common graphical constraints for all kinds of state * charts. * * @author terfloth * @author muelder * @author bohl - migrated to xtext infrastruture * @author schwertfeger * @author antony */ public class SGraphJavaValidator extends AbstractDeclarativeValidator { public static final String ISSUE_STATE_WITHOUT_NAME = "A state must have a name."; public static final String ISSUE_NODE_NOT_REACHABLE = "Node is not reachable."; public static final String ISSUE_FINAL_STATE_OUTGOING_TRANSITION = "A final state should have no outgoing transition."; public static final String ISSUE_STATE_WITHOUT_OUTGOING_TRANSITION = "A state should have at least one outgoing transition."; public static final String ISSUE_INITIAL_ENTRY_WITH_IN_TRANS = "Initial entry should have no incoming transition."; public static final String ISSUE_INITIAL_ENTRY_WITHOUT_OUT_TRANS = "Initial entry should have a single outgoing transition."; public static final String ISSUE_ENTRY_WITH_MULTIPLE_OUT_TRANS = "Entries must not have more than one outgoing transition."; public static final String ISSUE_ENTRY_WITH_TRIGGER = "Outgoing Transitions from Entries can not have a Trigger or Guard."; public static final String ISSUE_EXIT_WITH_OUT_TRANS = "Exit node should have no outgoing transition."; public static final String ISSUE_EXIT_WITHOUT_IN_TRANS = "Exit node should have at least one incoming transition."; public static final String ISSUE_EXIT_ON_STATECHART = "Exit node in top level region not supported - use final states instead."; public static final String ISSUE_CHOICE_WITHOUT_OUTGOING_TRANSITION = "A choice must have at least one outgoing transition."; public static final String ISSUE_REGION_CANT_BE_ENTERED_USING_SHALLOW_HISTORY_NO_DEFAULT_ENTRY = "The region can't be entered using the shallow history. Add a default entry node."; public static final String ISSUE_REGION_CANT_BE_ENTERED_USING_SHALLOW_HISTORY_NON_CONNECTED_DEFAULT_ENTRY = "The region can't be entered using the shallow history. Add a transition from default entry to a state."; public static final String ISSUE_SUBMACHINE_UNRESOLVABLE = "Referenced substate machine '%s'does not exist!"; public static final String ISSUE_SYNCHRONIZATION_TARGET_STATES_NOT_ORTHOGONAL = "The target states of a synchronization must be orthogonal!"; public static final String ISSUE_SYNCHRONIZATION_TARGET_STATES_NOT_WITHIN_SAME_PARENTSTATE = "The target states of a synchronization have to be contained in the same parent state within different regions!"; public static final String ISSUE_SYNCHRONIZATION_SOURCE_STATES_NOT_ORTHOGONAL = "The source states of a synchronization must be orthogonal!"; public static final String ISSUE_SYNCHRONIZATION_SOURCE_STATES_NOT_WITHIN_SAME_PARENTSTATE = "The source states of a synchronization have to be contained in the same parent state within different regions!"; public static final String ISSUE_SYNCHRONIZATION_TRANSITION_COUNT = "A synchronization should have at least two incoming or two outgoing transitions."; public static final String ISSUE_TRANSITION_ORTHOGONAL = "Source and target of a transition must not be located in orthogonal regions!"; public static final String ISSUE_INITIAL_ENTRY_WITH_TRANSITION_TO_CONTAINER = "Outgoing Transitions from Entries can only target to sibling or inner states."; public static final String ISSUE_STATECHART_NAME_NO_IDENTIFIER = "%s is not a valid identifier!"; @Check(CheckType.FAST) public void vertexNotReachable(final Vertex vertex) { if (!(vertex instanceof Entry)) { final Set<Object> stateScopeSet = new HashSet<Object>(); for (EObject obj : EcoreUtil2.eAllContents(vertex)) { stateScopeSet.add(obj); } stateScopeSet.add(vertex); final List<Object> externalPredecessors = new ArrayList<Object>(); DFS dfs = new DFS() { @Override public Iterator<Object> getElementLinks(Object element) { List<Object> elements = new ArrayList<Object>(); if (element instanceof org.yakindu.sct.model.sgraph.State) { if (!stateScopeSet.contains(element)) { externalPredecessors.add(element); } else { elements.addAll(((org.yakindu.sct.model.sgraph.State) element).getRegions()); elements.addAll(((org.yakindu.sct.model.sgraph.State) element).getIncomingTransitions()); } } else if (element instanceof Region) { elements.addAll(((Region) element).getVertices()); } else if (element instanceof Entry) { if (!stateScopeSet.contains(element)) { externalPredecessors.add(element); } else { elements.addAll(((Entry) element).getIncomingTransitions()); } } else if (element instanceof Vertex) { elements.addAll(((Vertex) element).getIncomingTransitions()); } else if (element instanceof Transition) { elements.add(((Transition) element).getSource()); } return elements.iterator(); } }; dfs.perform(vertex); if (externalPredecessors.size() == 0) { error(ISSUE_NODE_NOT_REACHABLE, vertex, null, -1); } } } /** * Calculates all predecessor states */ @Check(CheckType.FAST) public void finalStateWithOutgoingTransition(FinalState finalState) { if ((finalState.getOutgoingTransitions().size() > 0)) { warning(ISSUE_FINAL_STATE_OUTGOING_TRANSITION, finalState, null, -1); } } @Check(CheckType.FAST) public void nameIsNotEmpty(org.yakindu.sct.model.sgraph.State state) { if ((state.getName() == null || state.getName().trim().length() == 0) && !(state instanceof FinalState)) { error(ISSUE_STATE_WITHOUT_NAME, state, null, -1); } } @Check(CheckType.FAST) public void choiceWithoutOutgoingTransition(Choice choice) { // Choice without outgoing transition if (choice.getOutgoingTransitions().size() == 0) { error(ISSUE_CHOICE_WITHOUT_OUTGOING_TRANSITION, choice, null, -1); } } @Check(CheckType.FAST) public void disallowTrigger(Entry entry) { for (Transition transition : entry.getOutgoingTransitions()) { if (transition.getTrigger() != null) { error(ISSUE_ENTRY_WITH_TRIGGER, entry, null, -1); } } } @Check(CheckType.FAST) public void initialEntryWithoutIncomingTransitions(Entry entry) { if (entry.getIncomingTransitions().size() > 0 && entry.getKind().equals(EntryKind.INITIAL)) { warning(ISSUE_INITIAL_ENTRY_WITH_IN_TRANS, entry, null, -1); } } @Check(CheckType.FAST) public void initialEntryWithoutOutgoingTransition(Entry entry) { if (entry.getOutgoingTransitions().size() == 0 && ((Entry) entry).getKind().equals(EntryKind.INITIAL)) { warning(ISSUE_INITIAL_ENTRY_WITHOUT_OUT_TRANS, entry, null, -1); } } @Check(CheckType.FAST) public void initialEntryWithMultipleOutgoingTransition(Entry entry) { if (entry.getOutgoingTransitions().size() > 1) { error(ISSUE_ENTRY_WITH_MULTIPLE_OUT_TRANS, entry, null, -1); } } @Check(CheckType.FAST) public void exitWithoutIncomingTransition(Exit exit) { if (exit.getIncomingTransitions().size() == 0) { warning(ISSUE_EXIT_WITHOUT_IN_TRANS, exit, null, -1); } } @Check(CheckType.FAST) public void exitWithOutgoingTransition(Exit exit) { if (exit.getOutgoingTransitions().size() > 0) { error(ISSUE_EXIT_WITH_OUT_TRANS, exit, null, -1); } } /** * Exit nodes in top level regions are not supported. * * @param exit */ @Check(CheckType.FAST) public void exitOnStatechart(Exit exit) { if (exit.getParentRegion().getComposite() instanceof Statechart) { error(ISSUE_EXIT_ON_STATECHART, exit, null, -1); } } @Check(CheckType.FAST) public void synchronizationTransitionCount(Synchronization sync) { if (sync.getIncomingTransitions().size() < 2 && sync.getOutgoingTransitions().size() < 2) { warning(ISSUE_SYNCHRONIZATION_TRANSITION_COUNT, sync, null, -1); } } @Check(CheckType.FAST) public void initialEntryWithTransitionToContainer(Transition t) { if (t.getSource() instanceof Entry && !isChildOrSibling(t.getSource(), t.getTarget())) { error(ISSUE_INITIAL_ENTRY_WITH_TRANSITION_TO_CONTAINER, t, null, -1); } } private boolean isChildOrSibling(Vertex source, Vertex target) { TreeIterator<EObject> iter = source.getParentRegion().eAllContents(); while (iter.hasNext()) { if (target == iter.next()) { return true; } } return false; } /** * Checks if all composite states that are siblings of a shallow history can * enter their regions. * * @param e */ @Check(CheckType.FAST) public void regionCantBeEnteredUsingShallowHistory(Entry e) { if (e.getKind() == EntryKind.SHALLOW_HISTORY) { // get all regions off all sibling states List<Region> regions = new ArrayList<Region>(); for (Vertex v : e.getParentRegion().getVertices()) { if (v instanceof org.yakindu.sct.model.sgraph.State) { org.yakindu.sct.model.sgraph.State state = (org.yakindu.sct.model.sgraph.State) v; regions.addAll(state.getRegions()); } } // check each region for (Region r : regions) { // first determine if the region contains a default entry Entry defaultEntry = null; for (Vertex v : r.getVertices()) { if (v instanceof Entry) { String name = v.getName().trim().toLowerCase(); if (name != null || "".equals(name) || "default".equals(name)) { defaultEntry = (Entry) v; break; } } } // now check error conditions if (defaultEntry == null) { error(ISSUE_REGION_CANT_BE_ENTERED_USING_SHALLOW_HISTORY_NO_DEFAULT_ENTRY, r, null, -1); } else if (defaultEntry.getOutgoingTransitions().size() != 1) { error(ISSUE_REGION_CANT_BE_ENTERED_USING_SHALLOW_HISTORY_NON_CONNECTED_DEFAULT_ENTRY, r, null, -1); } } } } @Check public void orthogonalTransition(Transition transition) { Vertex source = transition.getSource(); Vertex target = transition.getTarget(); if ((source instanceof Synchronization) || (target instanceof Synchronization)) return; // ... the check does not apply. EObject commonAncestor = commonAncestor(source, target); if (commonAncestor instanceof CompositeElement) { error(ISSUE_TRANSITION_ORTHOGONAL, transition, null, -1); } } @Check public void orthogonalSourceStates(Synchronization sync) { List<Vertex> sourceVertices = sources(sync.getIncomingTransitions()); if (!areOrthogonal(sourceVertices)) { error(ISSUE_SYNCHRONIZATION_SOURCE_STATES_NOT_ORTHOGONAL, sync, null, -1); } } @Check public void orthogonalTargetStates(Synchronization sync) { List<Vertex> sourceVertices = targets(sync.getOutgoingTransitions()); if (!areOrthogonal(sourceVertices)) { error(ISSUE_SYNCHRONIZATION_TARGET_STATES_NOT_ORTHOGONAL, sync, null, -1); } } @Check public void orthogonalSynchronizedTransition(Synchronization sync) { List<Transition> incoming = sync.getIncomingTransitions(); List<List<EObject>> inAncestorsList = new ArrayList<List<EObject>>(); for (Transition trans : incoming) { inAncestorsList.add(collectAncestors(trans.getSource(), new ArrayList<EObject>())); } List<Transition> outgoing = sync.getOutgoingTransitions(); List<List<EObject>> outAncestorsList = new ArrayList<List<EObject>>(); for (Transition trans : outgoing) { outAncestorsList.add(collectAncestors(trans.getTarget(), new ArrayList<EObject>())); } Set<Transition> inOrthogonal = new HashSet<Transition>(incoming); Set<Transition> outOrthogonal = new HashSet<Transition>(outgoing); for (int i = 0; i < incoming.size(); i++) { for (int j = 0; j < outgoing.size(); j++) { EObject commonAncestor = findCommonAncestor(inAncestorsList.get(i), outAncestorsList.get(j)); if (commonAncestor instanceof Region) { inOrthogonal.remove(incoming.get(i)); outOrthogonal.remove(outgoing.get(j)); } } } for (Transition trans : inOrthogonal) { error(ISSUE_SYNCHRONIZATION_SOURCE_STATES_NOT_WITHIN_SAME_PARENTSTATE, trans, null, -1); } for (Transition trans : outOrthogonal) { error(ISSUE_SYNCHRONIZATION_TARGET_STATES_NOT_WITHIN_SAME_PARENTSTATE, trans, null, -1); } } @Check public void checkStatechartNameIsIdentifier(Statechart statechart) { if (!isValidJavaIdentifier(statechart.getName())) { error(String.format(ISSUE_STATECHART_NAME_NO_IDENTIFIER, statechart.getName()), statechart, BasePackage.Literals.NAMED_ELEMENT__NAME, -1); } } protected boolean isValidJavaIdentifier(String s) { if (s == null || s.length() == 0) { return false; } char[] c = s.toCharArray(); if (!Character.isJavaIdentifierStart(c[0])) { return false; } for (int i = 1; i < c.length; i++) { if (!Character.isJavaIdentifierPart(c[i])) { return false; } } return true; } @Override public boolean isLanguageSpecific() { return false; } @Inject public void register(EValidatorRegistrar registrar) { // Do not register because this validator is only a composite #398987 } }