/*******************************************************************************
* Copyright (c) 2012, 2016 Obeo.
* 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:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.emf.compare.rcp.ui.internal.structuremergeviewer.groups.impl;
import static com.google.common.collect.Collections2.filter;
import static com.google.common.collect.Iterators.any;
import static com.google.common.collect.Iterators.concat;
import static com.google.common.collect.Iterators.transform;
import static com.google.common.collect.Lists.newArrayList;
import static org.eclipse.emf.compare.utils.EMFComparePredicates.hasState;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.Conflict;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.DifferenceState;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.MatchResource;
import org.eclipse.emf.compare.ReferenceChange;
import org.eclipse.emf.compare.ResourceAttachmentChange;
import org.eclipse.emf.compare.provider.utils.ComposedStyledString;
import org.eclipse.emf.compare.provider.utils.IStyledString;
import org.eclipse.emf.compare.provider.utils.IStyledString.Style;
import org.eclipse.emf.compare.rcp.ui.EMFCompareRCPUIPlugin;
import org.eclipse.emf.compare.rcp.ui.internal.EMFCompareRCPUIMessages;
import org.eclipse.emf.compare.rcp.ui.internal.structuremergeviewer.nodes.ConflictNode;
import org.eclipse.emf.compare.rcp.ui.internal.structuremergeviewer.nodes.DiffNode;
import org.eclipse.emf.compare.rcp.ui.internal.structuremergeviewer.nodes.MatchNode;
import org.eclipse.emf.compare.rcp.ui.internal.structuremergeviewer.nodes.MatchResourceNode;
import org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroup;
import org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.extender.IDifferenceGroupExtender;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.util.ECrossReferenceAdapter;
import org.eclipse.emf.edit.tree.TreeNode;
import org.eclipse.swt.graphics.Image;
/**
* This implementation of a {@link IDifferenceGroup} uses a predicate to filter the whole list of differences.
* <p>
* This can be subclasses or used directly instead of {@link IDifferenceGroup}.
* </p>
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
* @since 4.0
*/
public class BasicDifferenceGroupImpl extends AdapterImpl implements IDifferenceGroup {
/**
* Function that returns all contents of the given EObject.
*/
protected static final Function<EObject, Iterator<EObject>> E_ALL_CONTENTS = new Function<EObject, Iterator<EObject>>() {
public Iterator<EObject> apply(EObject eObject) {
return eObject.eAllContents();
}
};
/** The filter we'll use in order to filter the differences that are part of this group. */
protected final Predicate<? super Diff> filter;
/** The name that the EMF Compare UI will display for this group. */
protected final String name;
/** The icon that the EMF Compare UI will display for this group. */
protected final Image image;
/** The list of children of this group. */
protected List<TreeNode> children;
/** The comparison that is the parent of this group. */
private final Comparison comparison;
/** The registry of difference group extenders. */
private final IDifferenceGroupExtender.Registry registry = EMFCompareRCPUIPlugin.getDefault()
.getDifferenceGroupExtenderRegistry();
/** The cross reference adapter that will be added to this group's children. */
private final ECrossReferenceAdapter crossReferenceAdapter;
/**
* Instantiates this group given the comparison and filter that should be used in order to determine its
* list of differences.
* <p>
* This will use the default name and icon for the group.
* </p>
*
* @param comparison
* The comparison that is the parent of this group.
* @param filter
* The filter we'll use in order to filter the differences that are part of this group.
* @param crossReferenceAdapter
* The cross reference adapter that will be added to this group's children.
*/
public BasicDifferenceGroupImpl(Comparison comparison, Predicate<? super Diff> filter,
ECrossReferenceAdapter crossReferenceAdapter) {
this(comparison, filter, EMFCompareRCPUIMessages.getString("BasicDifferenceGroup.name"), //$NON-NLS-1$
EMFCompareRCPUIPlugin.getImage("icons/full/toolb16/group.gif"), //$NON-NLS-1$
crossReferenceAdapter);
}
/**
* Instantiates this group given the comparison and filter that should be used in order to determine its
* list of differences. It will be displayed in the UI with the default icon and the given name.
*
* @param comparison
* The comparison that is the parent of this group.
* @param filter
* The filter we'll use in order to filter the differences that are part of this group.
* @param name
* The name that the EMF Compare UI will display for this group.
* @param crossReferenceAdapter
* The cross reference adapter that will be added to this group's children.
*/
public BasicDifferenceGroupImpl(Comparison comparison, Predicate<? super Diff> filter, String name,
ECrossReferenceAdapter crossReferenceAdapter) {
this(comparison, filter, name, EMFCompareRCPUIPlugin.getImage("icons/full/toolb16/group.gif"), //$NON-NLS-1$
crossReferenceAdapter);
}
/**
* Instantiates this group given the comparison and filter that should be used in order to determine its
* list of differences. It will be displayed in the UI with the given icon and name.
*
* @param comparison
* The comparison that is the parent of this group.
* @param filter
* The filter we'll use in order to filter the differences that are part of this group.
* @param name
* The name that the EMF Compare UI will display for this group.
* @param image
* The icon that the EMF Compare UI will display for this group.
* @param crossReferenceAdapter
* Updated upstream The cross reference adapter that will be added to this group's children.
*/
public BasicDifferenceGroupImpl(Comparison comparison, Predicate<? super Diff> filter, String name,
Image image, ECrossReferenceAdapter crossReferenceAdapter) {
this.comparison = comparison;
this.filter = filter;
this.name = name;
this.image = image;
this.crossReferenceAdapter = crossReferenceAdapter;
}
/**
* Returns the comparison object.
*
* @return the comparison object.
*/
protected final Comparison getComparison() {
return comparison;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.common.notify.impl.AdapterImpl#isAdapterForType(java.lang.Object)
*/
@Override
public boolean isAdapterForType(Object type) {
return type == IDifferenceGroup.class;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroup#getName()
*/
public String getName() {
return name;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroup#getStyledName()
*/
public IStyledString.IComposedStyledString getStyledName() {
final IStyledString.IComposedStyledString ret = new ComposedStyledString();
Iterator<EObject> eAllContents = concat(transform(getChildren().iterator(), E_ALL_CONTENTS));
Iterator<EObject> eAllData = transform(eAllContents, TREE_NODE_DATA);
boolean unresolvedDiffs = any(Iterators.filter(eAllData, Diff.class),
hasState(DifferenceState.UNRESOLVED));
if (unresolvedDiffs) {
ret.append("> ", Style.DECORATIONS_STYLER); //$NON-NLS-1$
}
ret.append(getName());
return ret;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroup#getImage()
*/
public Image getImage() {
return image;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroup#getChildren()
*/
public List<? extends TreeNode> getChildren() {
if (children == null) {
buildSubTree();
}
return children;
}
/**
* Registers the CrossReferenceAdapter to all given notifiers.
*
* @param notifiers
* the list of notifiers.
*/
protected final void registerCrossReferenceAdapter(List<? extends Notifier> notifiers) {
for (Notifier notifier : notifiers) {
// this cross referencer has to live as long as the objects on which it is installed.
notifier.eAdapters().add(crossReferenceAdapter);
}
}
/**
* Unregisters the CrossReferenceAdapter from all given notifiers.
*
* @param notifiers
* the list of notifiers.
*/
protected final void unregisterCrossReferenceAdapter(List<? extends Notifier> notifiers) {
for (Notifier notifier : notifiers) {
// this cross referencer has to live as long as the objects on which it is installed.
notifier.eAdapters().remove(crossReferenceAdapter);
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroup#dispose()
*/
public void dispose() {
if (children != null) {
unregisterCrossReferenceAdapter(children);
children = null;
}
}
/**
* Builds the sub tree for this group.
*/
public void buildSubTree() {
children = createChildren();
doBuildSubTrees();
customize(children);
registerCrossReferenceAdapter(children);
}
/**
* Perform the creation of the sub-trees of the group.
*/
protected void doBuildSubTrees() {
children.addAll(buildMatchTrees());
children.addAll(buildMatchResourceTrees());
}
/**
* This creates the root-level children of the group.
*
* @return This default implementation returns a new ArrayList. It may be overridden by sub-classes.
*/
protected List<TreeNode> createChildren() {
return newArrayList();
}
/**
* Compute a subTree for each root match of the comparison.
*
* @return the list of matchSubTrees
*/
protected List<TreeNode> buildMatchTrees() {
final List<TreeNode> matchTrees = new ArrayList<TreeNode>();
for (Match match : getComparison().getMatches()) {
MatchNode matchNode = buildTree(match);
if (matchNode != null) {
matchTrees.add(matchNode);
}
}
return matchTrees;
}
/**
* Compute a tree for the given match.
*
* @param match
* The given match
* @return a list of subTree for this match, must not be <code>null</code>
*/
protected MatchNode buildTree(Match match) {
MatchNode result = null;
MatchNode matchNode = createMatchNode(match);
populateMatchNode(matchNode);
if (!matchNode.getChildren().isEmpty()) {
result = matchNode;
}
return result;
}
/**
* Build the subtree for the given match.
*
* @param matchNode
* The root matchNode
* @return the computed matchNode
*/
protected void populateMatchNode(MatchNode matchNode) {
Match match = matchNode.getMatch();
Multimap<Match, Diff> diffsBySubMatch = LinkedHashMultimap.create();
for (Diff diff : filter(match.getDifferences(), filter)) {
// If a diff is part of a larger diff (is refined by), we don't want to add it to the tree. It
// will be added by the algorithm in a second step. This way we avoid duplication and all diffs
// that are part of a 'master' diff are grouped as children of this 'master' diff
if (mustDisplayAsDirectChildOfMatch(diff)) {
Match targetMatch = getTargetMatch(diff);
if (match == targetMatch) {
addDiffNode(matchNode, diff);
} else if (match.getSubmatches().contains(targetMatch)) {
diffsBySubMatch.put(targetMatch, diff);
} else if (targetMatch != null) {
MatchNode targetMatchNode = createMatchNode(targetMatch);
matchNode.addSubMatchNode(targetMatchNode);
addDiffNode(targetMatchNode, diff);
}
}
}
for (Match subMatch : match.getSubmatches()) {
MatchNode subMatchNode = createMatchNode(subMatch);
for (Diff subMatchDiff : diffsBySubMatch.get(subMatch)) {
addDiffNode(subMatchNode, subMatchDiff);
}
diffsBySubMatch.removeAll(subMatch);
populateMatchNode(subMatchNode);
if (!subMatchNode.getChildren().isEmpty()) {
matchNode.addSubMatchNode(subMatchNode);
}
}
}
/**
* Provide the Match that should directly contain the given diff. If the given diff should not be a direct
* child of a Match, the method must return <code>null</code>. For a given strategy, a diff should only be
* displayed in the same Match (i.e. the {@link DiffNode}s that represent the diff should always be
* children of the {@link MatchNode}s that represent the returned Match.
*
* @param diff
* The difference
* @return The Match that is a direct parent of the given diff, can be <code>null</code>.
*/
protected Match getTargetMatch(Diff diff) {
if (mustDisplayAsDirectChildOfMatch(diff)) {
if (isContainmentRefChange(diff)) {
Match valueMatch = diff.getMatch().getComparison()
.getMatch(((ReferenceChange)diff).getValue());
return valueMatch; // This match may not be a sub-match because the child may have moved
} else if (isContainmentRefChange(diff.getPrimeRefining())) {
Match valueMatch = diff.getMatch().getComparison()
.getMatch(((ReferenceChange)diff.getPrimeRefining()).getValue());
return valueMatch; // This match may not be a sub-match because the child may have moved
}
return diff.getMatch();
}
return null;
}
/**
* Does the given difference have to be displayed as direct child of a Match?
*
* @param diff
* The diff
* @return <code>true</code> if the diff's node should be a child of a MatchNode.
*/
protected boolean mustDisplayAsDirectChildOfMatch(Diff diff) {
return diff.getRefines().isEmpty();
}
/**
* Is it a containment reference change?
*
* @param diff
* The diff
* @return <code>true</code> if the diff is a {@link ReferenceChange} whose {@link EReference} is a
* containment reference.
*/
protected boolean isContainmentRefChange(Diff diff) {
return diff instanceof ReferenceChange && ((ReferenceChange)diff).getReference().isContainment();
}
protected List<TreeNode> buildMatchResourceTrees() {
final List<TreeNode> matchResourceTrees = new ArrayList<TreeNode>();
if (getComparison().getMatchedResources().isEmpty()) {
return matchResourceTrees;
}
final Iterable<ResourceAttachmentChange> attachmentChanges = Iterables
.filter(getComparison().getDifferences(), ResourceAttachmentChange.class);
final Multimap<String, ResourceAttachmentChange> uriToRAC = LinkedHashMultimap.create();
for (ResourceAttachmentChange attachmentChange : attachmentChanges) {
uriToRAC.put(attachmentChange.getResourceURI(), attachmentChange);
}
for (MatchResource matchResource : getComparison().getMatchedResources()) {
final Collection<ResourceAttachmentChange> leftRAC = uriToRAC.get(matchResource.getLeftURI());
final Collection<ResourceAttachmentChange> rightRAC = uriToRAC.get(matchResource.getRightURI());
final Collection<ResourceAttachmentChange> originRAC = uriToRAC.get(matchResource.getOriginURI());
final LinkedHashSet<ResourceAttachmentChange> racForMatchResource = Sets.newLinkedHashSet();
racForMatchResource.addAll(leftRAC);
racForMatchResource.addAll(rightRAC);
racForMatchResource.addAll(originRAC);
MatchResourceNode matchNode = buildSubTree(matchResource, racForMatchResource);
if (matchNode != null) {
matchResourceTrees.add(matchNode);
}
}
return matchResourceTrees;
}
/**
* Build the sub tree of the given {@link MatchResource}.
*
* @param matchResource
* the given MatchResource.
* @return the sub tree of the given MatchResource.
*/
protected MatchResourceNode buildSubTree(MatchResource matchResource,
Set<ResourceAttachmentChange> attachmentChanges) {
MatchResourceNode matchResourceNode = createMatchResourceNode(matchResource);
Collection<ResourceAttachmentChange> filteredChanges = filter(attachmentChanges, filter);
for (ResourceAttachmentChange attachmentChange : filteredChanges) {
DiffNode diffNode = createDiffNode(attachmentChange);
matchResourceNode.addDiffNode(diffNode);
}
return matchResourceNode;
}
/**
* Add the diff in the given match. This method handles refined diffs and allows to customize the result.
*
* @param matchNode
* The given match node
* @param diff
* The diff to add
*/
protected void addDiffNode(MatchNode matchNode, Diff diff) {
if (!(diff instanceof ResourceAttachmentChange)) {
DiffNode diffNode = createDiffNode(diff);
handleRefiningDiffs(diffNode);
matchNode.addDiffNode(diffNode);
}
}
/**
* Create a diff node.
*
* @param diff
* The given diff
* @return the DiffNode
*/
protected DiffNode createDiffNode(Diff diff) {
DiffNode diffNode = new DiffNode(diff);
diffNode.eAdapters().add(this);
return diffNode;
}
/**
* Create a match node.
*
* @param match
* The given match
* @return the MatchNode
*/
protected MatchNode createMatchNode(Match match) {
MatchNode matchNode = new MatchNode(match);
matchNode.eAdapters().add(this);
return matchNode;
}
/**
* Create a conflict node.
*
* @param conflict
* The given conflict
* @return the ConflictNode
*/
protected ConflictNode createConflictNode(Conflict conflict) {
ConflictNode conflictNode = new ConflictNode(conflict);
conflictNode.eAdapters().add(this);
return conflictNode;
}
/**
* Create a matchResource node.
*
* @param matchResource
* The given matchResource
* @return the MatchResourceNode
*/
protected MatchResourceNode createMatchResourceNode(MatchResource matchResource) {
MatchResourceNode matchResourceNode = new MatchResourceNode(matchResource);
matchResourceNode.eAdapters().add(this);
return matchResourceNode;
}
/**
* Walk the given trees and customize each node in the tree, starting by the deeper nodes all the way up
* to the root nodes. This method calls itself recursively.
*
* @param nodes
* The list of nodes to customize.
*/
protected void customize(List<? extends TreeNode> nodes) {
for (TreeNode node : nodes) {
customize(node.getChildren());
customize(node);
}
}
/**
* Allow extenders to customize a TreeNode.
*
* @param treeNode
* the TreeNode to customize.
*/
protected void customize(TreeNode treeNode) {
for (IDifferenceGroupExtender ext : registry.getExtenders()) {
if (ext.handle(treeNode)) {
ext.addChildren(treeNode);
}
}
}
/**
* Handle the diffs that refine the given diff. Refining diffs are added as children of the given diff,
* and so on recursively.
*
* @param diffNode
* The diff node to handle, which is not necessarily a child of a MatchNode since this method
* is called recursively.
*/
protected void handleRefiningDiffs(DiffNode diffNode) {
Diff diff = diffNode.getDiff();
for (Diff refiningDiff : diff.getRefinedBy()) {
DiffNode refinedDiffNode = createDiffNode(refiningDiff);
diffNode.addRefinedDiffNode(refinedDiffNode);
handleRefiningDiffs(refinedDiffNode);
}
}
}