| /***************************************************************************** |
| * Copyright (c) 2019 CEA LIST, and others. |
| * |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Nicolas FAUVERGUE (CEA LIST) nicolas.fauvergue@cea.fr - Initial API and implementation |
| * |
| *****************************************************************************/ |
| package org.eclipse.papyrus.gitlight.compare.ui.internal.structuremergeviewer.groups; |
| |
| import static com.google.common.base.Predicates.not; |
| import static com.google.common.collect.Collections2.filter; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Predicate; |
| import com.google.common.base.Predicates; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.runtime.IAdaptable; |
| import org.eclipse.emf.common.util.ECollections; |
| import org.eclipse.emf.compare.Comparison; |
| import org.eclipse.emf.compare.Diff; |
| import org.eclipse.emf.compare.Match; |
| import org.eclipse.emf.compare.MatchResource; |
| import org.eclipse.emf.compare.rcp.ui.internal.structuremergeviewer.groups.impl.BasicDifferenceGroupImpl; |
| import org.eclipse.emf.compare.rcp.ui.internal.structuremergeviewer.nodes.MatchNode; |
| import org.eclipse.emf.compare.rcp.ui.mergeviewer.IMergeViewer.MergeViewerSide; |
| import org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.AbstractDifferenceGroupProvider; |
| import org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroup; |
| import org.eclipse.emf.compare.scope.IComparisonScope; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.resource.ResourceSet; |
| import org.eclipse.emf.ecore.util.ECrossReferenceAdapter; |
| import org.eclipse.emf.edit.tree.TreeFactory; |
| import org.eclipse.emf.edit.tree.TreeNode; |
| import org.eclipse.papyrus.gitlight.compare.ui.contentmergeviewer.facet.PapyrusFacetContentProviderWrapper; |
| import org.eclipse.papyrus.gitlight.compare.ui.internal.context.PapyrusContextUtil; |
| import org.eclipse.papyrus.infra.viewpoints.policy.ViewPrototype; |
| import org.eclipse.papyrus.uml.tools.providers.SemanticUMLContentProvider; |
| |
| /** |
| * This implementation of a |
| * {@link org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroupProvider} will be used to |
| * group the differences of Papyrus models. |
| */ |
| @SuppressWarnings("restriction") |
| public class PapyrusGroupProvider extends AbstractDifferenceGroupProvider { |
| |
| /** |
| * Predicate testing whether a {@link MatchResource} is a UML resource (file extension is .uml). |
| */ |
| private static final Predicate<? super MatchResource> IS_UML_RESOURCE = new Predicate<MatchResource>() { |
| private final Predicate<Resource> isUML = PapyrusContextUtil.isUMLResource(); |
| |
| public boolean apply(MatchResource input) { |
| return input != null // |
| && (isUML.apply(input.getLeft()) || isUML.apply(input.getRight()) |
| || isUML.apply(input.getOrigin())); |
| } |
| }; |
| |
| /** |
| * Specialized {@link BasicDifferenceGroupImpl} for Papyrus models. |
| */ |
| public static class PapyrusDifferenceGroup extends BasicDifferenceGroupImpl { |
| |
| /** Map for retrieving the corresponding TreeNode of a model element. **/ |
| private Map<EObject, TreeNode> valueToMatchNode; |
| |
| /** Map for retrieving the context UML element of a notation (Diagram/Table/etc.). **/ |
| private HashMap<EObject, EObject> notationToContextualParent; |
| |
| /** A predicate testing whether a match matches notations (Diagrams/Tables/etc.). **/ |
| private final Predicate<Match> matchValueNotationPredicate = new Predicate<Match>() { |
| public boolean apply(Match match) { |
| EObject matchValue = getMatchValue(match); |
| return (matchValue != null) && ViewPrototype.isViewObject(matchValue); |
| } |
| }; |
| |
| /** |
| * Initializes me with my comparison and cross-referencer. |
| * |
| * @param comparison |
| * the comparison being presented |
| * @param crossReferenceAdapter |
| * a cross-reference adapter to propagate down the tree |
| */ |
| public PapyrusDifferenceGroup(Comparison comparison, ECrossReferenceAdapter crossReferenceAdapter) { |
| super(comparison, Predicates.<Diff> alwaysTrue(), crossReferenceAdapter); |
| valueToMatchNode = new HashMap<>(); |
| notationToContextualParent = new HashMap<>(); |
| retrieveContextualNotationParents(MergeViewerSide.LEFT); |
| retrieveContextualNotationParents(MergeViewerSide.RIGHT); |
| } |
| |
| /** |
| * Makes use of the PapyrusFacetContentProviderWrapper to retrieve the contextual UML element for each |
| * notation of the Comparison. |
| * |
| * @param side |
| * Defines the comparison side for retrieving the elements |
| */ |
| private void retrieveContextualNotationParents(MergeViewerSide side) { |
| ResourceSet resourceSet = getResourceSet(getComparison(), side); |
| if (resourceSet != null) { |
| SemanticUMLContentProvider contentProvider = new SemanticUMLContentProvider(resourceSet); |
| try { |
| for (Object root : contentProvider.getElements(null)) { |
| retrieveNotationParents(root, contentProvider); |
| } |
| } finally { |
| contentProvider.dispose(); |
| } |
| } |
| } |
| |
| /** |
| * Recursively traverses the UML mode tree and maps all notations and their contextual parent. |
| * |
| * @param object |
| * the child Object |
| * @param facetContentProviderWrapper |
| * the {@link PapyrusFacetContentProviderWrapper} |
| */ |
| private void retrieveNotationParents(Object object, SemanticUMLContentProvider contentProvider) { |
| Function<Object, EObject> asEObject = adapt(EObject.class); |
| |
| // These children are EMF Facet TreeObjects |
| for (Object child : contentProvider.getChildren(object)) { |
| // Get the encapsulated model object |
| EObject eObjectChild = asEObject.apply(child); |
| if (eObjectChild != null) { |
| // This is a legitimate case |
| EObject eObjectParent = asEObject.apply(object); |
| if (eObjectParent != null && ViewPrototype.isViewObject(eObjectChild)) { |
| if (!notationToContextualParent.containsKey(eObjectChild)) { |
| notationToContextualParent.put(eObjectChild, eObjectParent); |
| } |
| } |
| retrieveNotationParents(child, contentProvider); |
| } |
| } |
| } |
| |
| static <T> Function<Object, T> adapt(final Class<T> adapterType) { |
| return new Function<Object, T>() { |
| public T apply(Object input) { |
| if (adapterType.isInstance(input)) { |
| return adapterType.cast(input); |
| } else if (input instanceof IAdaptable) { |
| return ((IAdaptable)input).getAdapter(adapterType); |
| } else { |
| return null; |
| } |
| } |
| }; |
| } |
| |
| @Override |
| protected List<TreeNode> buildMatchTrees() { |
| final List<TreeNode> matchTrees = new ArrayList<TreeNode>(); |
| |
| final Collection<Match> matches = getComparison().getMatches(); |
| |
| /* |
| * Notation matches rely on information of other match trees for proper grouping (valueToMatchNode |
| * map needs to be populated ). So first all match trees for non-notation matches are built and |
| * the notation match sub trees are built separately in a second iteration. |
| */ |
| for (Match match : filter(matches, not(isMatchValueANotation()))) { |
| MatchNode matchNode = buildTree(match); |
| if (matchNode != null) { |
| matchTrees.add(matchNode); |
| } |
| } |
| |
| // But, we still want to present all notation views first as in the Model Explorer |
| List<TreeNode> nodesToSort = Lists.newArrayList(); |
| for (Match match : filter(matches, isMatchValueANotation())) { |
| TreeNode notationNode = buildTree(match); |
| if (notationNode != null) { |
| TreeNode parentNode = retrieveParentNode(notationNode, getParent(getMatchValue(match))); |
| parentNode.getChildren().add(notationNode); |
| notationNode.setParent(parentNode); |
| nodesToSort.add(parentNode); // This needs sorting |
| } |
| } |
| |
| if (!nodesToSort.isEmpty()) { |
| Comparator<TreeNode> sortBy = treeNodeOrdering(); |
| for (TreeNode next : nodesToSort) { |
| ECollections.sort(next.getChildren(), sortBy); |
| } |
| } |
| |
| return matchTrees; |
| } |
| |
| /** |
| * Obtain a comparator for sorting tree nodes by kind: |
| * <ol> |
| * <li>diffs of the object represented by the parent node (its own details) should come before |
| * anything to do with child objects</li> |
| * <li>notation views (diagrams, tables, etc.)</li> |
| * <li>all other children</li> |
| * </ol> |
| * |
| * @return the tree node comparator |
| */ |
| protected Comparator<TreeNode> treeNodeOrdering() { |
| return new Comparator<TreeNode>() { |
| public int compare(TreeNode o1, TreeNode o2) { |
| int key1 = computeSortKey(o1); |
| int key2 = computeSortKey(o2); |
| return key1 - key2; |
| } |
| |
| private int computeSortKey(TreeNode node) { |
| EObject object = node.getData(); |
| if (object instanceof Diff) { |
| // Diffs of the object should come before anything about children |
| return -2; |
| } |
| |
| if (object instanceof Match // |
| && isMatchValueANotation().apply((Match)object)) { |
| |
| // These come next |
| return -1; |
| } |
| |
| // Everything else follows |
| return 0; |
| } |
| }; |
| } |
| |
| /** |
| * Creates a predicate for determining if the associated EObject of a Match is an instance of the |
| * given class. |
| * |
| * @param clazz |
| * the Class |
| * @return the created Predicate |
| */ |
| private Predicate<Match> isMatchValueANotation() { |
| return matchValueNotationPredicate; |
| } |
| |
| /** |
| * Creates a new TreeNode. |
| * |
| * @param data |
| * data object of the node |
| * @return created TreeNode |
| */ |
| private TreeNode createTreeNode(EObject data) { |
| TreeNode node = TreeFactory.eINSTANCE.createTreeNode(); |
| node.setData(data); |
| return node; |
| } |
| |
| /** |
| * Determines the parent node of the given TreeNode. If the parent node doesn't exist yet the |
| * corresponding node and (sub) tree is created and merged into the match tree. |
| * |
| * @param childNode |
| * the child TreeNode |
| * @param parentObject |
| * the data object of the parent node |
| * @return (new) parent TreeNode |
| */ |
| private TreeNode retrieveParentNode(TreeNode childNode, EObject parentObject) { |
| TreeNode parentNode = getOrCreateParentNode(childNode, parentObject); |
| if (!(parentNode instanceof MatchNode)) { |
| // the parent node is a newly created node -> the corresponding subtree has to be created |
| createSubTree(parentNode, getParent(parentObject)); |
| } |
| return parentNode; |
| |
| } |
| |
| /** |
| * Retrieves the parent node of given TreeNode. If no matching node exists in the match tree a new |
| * node is created |
| * |
| * @param childNode |
| * the child TreeNode |
| * @param parentObject |
| * (new) parent TreeNode |
| * @return (new) parent TreeNode |
| */ |
| private TreeNode getOrCreateParentNode(TreeNode childNode, EObject parentObject) { |
| if (parentObject == null) { |
| // we have no parent to add this childNode; this may happen if a notation has no context; |
| // thus, |
| // we add it as root node (childNode is the parent without any parent) |
| return childNode; |
| } |
| |
| TreeNode parentNode = valueToMatchNode.get(parentObject); |
| if (parentNode == null) { |
| parentNode = createTreeNode(parentObject); |
| } |
| |
| childNode.setParent(parentNode); |
| parentNode.getChildren().add(childNode); |
| return parentNode; |
| } |
| |
| /** |
| * Retrieves the contextual parent of the given EObject. |
| * |
| * @param value |
| * the child EObject |
| * @return parent EObject |
| */ |
| private EObject getParent(EObject value) { |
| EObject result = notationToContextualParent.get(value); |
| if (result == null && value != null) { |
| result = value.eContainer(); |
| } |
| return result; |
| } |
| |
| /** |
| * Creates the subtree for the given tree node with a recursive bottom-up approach. |
| * |
| * @param treeNode |
| * the TreeNode |
| * @param parentObject |
| * the parent EObject |
| */ |
| private void createSubTree(TreeNode treeNode, EObject parentObject) { |
| if (parentObject == null) { |
| children.add(treeNode); |
| return; |
| } |
| TreeNode parentNode = getOrCreateParentNode(treeNode, parentObject); |
| treeNode.setParent(parentNode); |
| parentNode.getChildren().add(treeNode); |
| if (parentNode instanceof MatchNode) { |
| return; |
| } |
| createSubTree(parentNode, getParent(parentObject)); |
| } |
| |
| @Override |
| protected void populateMatchNode(MatchNode matchNode) { |
| super.populateMatchNode(matchNode); |
| if (!matchNode.getChildren().isEmpty()) { |
| valueToMatchNode.put(getMatchValue(matchNode.getMatch()), matchNode); |
| } |
| } |
| |
| /** |
| * Returns the corresponding EObject for the given Match. |
| * |
| * @param match |
| * the Match |
| * @return the corresponding EObject |
| */ |
| private EObject getMatchValue(Match match) { |
| if (match.getLeft() != null) { |
| return match.getLeft(); |
| } |
| if (match.getRight() != null) { |
| return match.getRight(); |
| } |
| return match.getOrigin(); |
| } |
| |
| /** |
| * Determines the object of the given match and side. |
| * |
| * @param comparison |
| * the {@link Comparison}. |
| * @param side |
| * the {@link MergeViewerSide}. |
| * @return the determined {@link Object}, may be {@code null}. |
| */ |
| private ResourceSet getResourceSet(Comparison comparison, MergeViewerSide side) { |
| for (MatchResource matchResource : comparison.getMatchedResources()) { |
| Resource resource = getResource(matchResource, side); |
| if (resource != null) { |
| return resource.getResourceSet(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Determines the resource of the given match and side. |
| * |
| * @param matchResource |
| * the {@link MatchResource}. |
| * @param side |
| * the {@link MergeViewerSide}. |
| * @return the determined {@link Resource}, may be {@code null}. |
| */ |
| private Resource getResource(MatchResource matchResource, MergeViewerSide side) { |
| if (side != null) { |
| switch (side) { |
| case LEFT: |
| return matchResource.getLeft(); |
| case RIGHT: |
| return matchResource.getRight(); |
| case ANCESTOR: |
| return matchResource.getOrigin(); |
| } |
| } |
| return null; |
| } |
| |
| } |
| |
| @Override |
| protected Collection<? extends IDifferenceGroup> buildGroups(Comparison comparison) { |
| PapyrusDifferenceGroup group = new PapyrusDifferenceGroup(getComparison(), |
| getCrossReferenceAdapter()); |
| group.buildSubTree(); |
| return ImmutableList.of(group); |
| } |
| |
| @Override |
| public boolean isEnabled(IComparisonScope scope, Comparison comparison) { |
| return super.isEnabled(scope, comparison) && PapyrusContextUtil.isPapyrusContext(comparison) |
| && hasAtLeastOneUMLResource(comparison); |
| } |
| |
| /** |
| * Specifies whether the given comparison has at least one UML resource in its matched resources. |
| * |
| * @param comparison |
| * The comparison to test. |
| * @return <code>true</code> if it has at least one {@link MatchResource} of a UML resource, |
| * <code>false</code> otherwise. |
| */ |
| private boolean hasAtLeastOneUMLResource(Comparison comparison) { |
| return Iterables.any(comparison.getMatchedResources(), IS_UML_RESOURCE); |
| } |
| |
| } |