blob: bed8e34d4eb84b0169c0eb28306123ff5c9a5746 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 EclipseSource Services GmbH 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:
* Philip Langer - initial API and implementation
*******************************************************************************/
package org.eclipse.emf.compare.ide.ui.internal.contentmergeviewer.property;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIMessages;
import org.eclipse.emf.compare.ide.ui.internal.configuration.EMFCompareConfiguration;
import org.eclipse.emf.compare.rcp.ui.internal.util.MergeViewerUtil;
import org.eclipse.emf.compare.rcp.ui.mergeviewer.IMergeViewer.MergeViewerSide;
import org.eclipse.emf.compare.rcp.ui.mergeviewer.item.IMergeViewerItem;
import org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroupProvider;
import org.eclipse.emf.compare.utils.IEqualityHelper;
import org.eclipse.emf.compare.utils.MatchUtil;
import org.eclipse.emf.compare.utils.ReferenceUtil;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.edit.provider.AdapterFactoryItemDelegator;
import org.eclipse.emf.edit.provider.IItemFontProvider;
import org.eclipse.emf.edit.provider.IItemPropertyDescriptor;
import org.eclipse.emf.edit.provider.IItemPropertySource;
import org.eclipse.emf.edit.provider.ITableItemFontProvider;
import org.eclipse.emf.edit.provider.ITableItemLabelProvider;
import org.eclipse.emf.edit.provider.ItemProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.widgets.TreeItem;
/**
* An {@link ItemProvider} used to represent each item in the property tree. It implements
* {@link IMergeViewerItem} to integrate with the underlying base framework. It is generally intended to work
* in a {@link TreeViewer tree}, with two columns, a 'Property' column and a 'Value' column, supporting an
* {@link #getColumnImage(Object, int) image} and {@link #getColumnText(Object, int) label} for each.
*/
@SuppressWarnings("deprecation")
abstract class PropertyItem extends ItemProvider implements ITableItemLabelProvider, ITableItemFontProvider, IMergeViewerItem.Container {
/** This is the special category name for property descriptors without a category. */
private static final String MISC_CATEGORY = EMFCompareIDEUIMessages
.getString("PropertyContentMergeViewer.miscCategory.label"); //$NON-NLS-1$
private static final String EXPERT_VIEW_FILTER_FLAG = "org.eclipse.ui.views.properties.expert"; //$NON-NLS-1$
/**
* The configuration used by the property item. It is used to know the
* {@link EMFCompareConfiguration#getComparison() comparison} and to get
* {@link EMFCompareConfiguration#getBooleanProperty(String, boolean) properties}.
*/
private EMFCompareConfiguration configuration;
/**
* The {@link #getSide() side} of this property item.
*/
private MergeViewerSide side;
/**
* The property item corresponding to the ancestor.
*/
protected PropertyItem ancestor;
/**
* The property item corresponding to the left.
*/
protected PropertyItem left;
/**
* The property item corresponding to the right.
*/
protected PropertyItem right;
/**
* Creates a root property item.
* <p>
* Builds a property item for the given object on the given side. This is used both to create a
* {@link #getRootItem() root} item and to build the children a property value that implements
* {@link IItemPropertySource}.
* </p>
*
* @param configuration
* the compare configuration of the root property item.
* @param object
* the side object for the root property item.
* @param side
* the side of this root property item.
* @return a new root property item.
*/
public static PropertyItem createPropertyItem(final EMFCompareConfiguration configuration,
final Object object, final MergeViewerSide side) {
PropertyItem rootItem;
List<IItemPropertyDescriptor> propertyDescriptors;
if (configuration.getAdapterFactory() != null) {
final AdapterFactoryItemDelegator itemDelegator = new AdapterFactoryItemDelegator(
configuration.getAdapterFactory());
rootItem = new RootPropertyItem(configuration, itemDelegator.getImage(object),
itemDelegator.getText(object), object, side);
propertyDescriptors = getPropertyDescriptors(object, itemDelegator);
} else {
// We're currently disposing of the property content merge viewer
rootItem = new RootPropertyItem(configuration, null, "", object, side); //$NON-NLS-1$
propertyDescriptors = null;
}
populateRootPropertyItem(rootItem, propertyDescriptors, object, configuration, side);
return rootItem;
}
private static List<IItemPropertyDescriptor> getPropertyDescriptors(final Object object,
final AdapterFactoryItemDelegator itemDelegator) {
List<IItemPropertyDescriptor> propertyDescriptors;
if (object instanceof Resource) {
// Special case for resources, because those generally have no property descriptors
propertyDescriptors = Collections
.singletonList(new ResourcePropertyDescriptor((Resource)object, itemDelegator));
} else {
propertyDescriptors = itemDelegator.getPropertyDescriptors(object);
}
return propertyDescriptors;
}
private static void populateRootPropertyItem(final PropertyItem rootItem,
final List<IItemPropertyDescriptor> propertyDescriptors, final Object object,
final EMFCompareConfiguration configuration, final MergeViewerSide side) {
if (propertyDescriptors == null) {
return;
}
Map<EStructuralFeature, Multimap<Object, Diff>> featureDiffs = buildFeatureToDiffMap(object,
configuration);
// A map from category name to a map from property name to the property descriptor item with
// that name. These both use tree maps to sort the categories and the property descriptors.
Map<String, Map<String, PropertyItem>> categories = Maps.newTreeMap();
for (IItemPropertyDescriptor propertyDescriptor : propertyDescriptors) {
addChildPropertyItem(categories, propertyDescriptor, object, configuration, featureDiffs, side);
}
// Compose the results into the children, do so with or without categories, as appropriate.
EList<PropertyItem> children = rootItem.getPropertyItems();
// If we're showing categories and there are categories, other than only the misc category...
if (shouldShowCategories(configuration) && (categories.size() > 1
|| categories.size() == 1 && categories.get(MISC_CATEGORY) == null)) {
// Build a category item for each category, adding it to the children, and add the
// property items as children of that category item.
for (Map.Entry<String, Map<String, PropertyItem>> entry : categories.entrySet()) {
PropertyItem categoryItem = new PropertyCategoryItem(configuration, entry.getKey(), side);
children.add(categoryItem);
categoryItem.getChildren().addAll(entry.getValue().values());
}
} else {
// Otherwise, compose all the categories into a single map and use those sorted property
// descriptor items as the children.
Map<String, PropertyItem> sortedItems = Maps.newTreeMap();
for (Map<String, PropertyItem> items : categories.values()) {
sortedItems.putAll(items);
}
children.addAll(sortedItems.values());
}
}
private static void addChildPropertyItem(Map<String, Map<String, PropertyItem>> categories,
IItemPropertyDescriptor itemPropertyDescriptor, Object object,
EMFCompareConfiguration configuration,
Map<EStructuralFeature, Multimap<Object, Diff>> featureDiffs, MergeViewerSide side) {
// If we're not showing advanced properties, skip the property descriptors flagged as
// expert properties.
if (!shouldShowAdvancedProperties(configuration)) {
String[] filterFlags = itemPropertyDescriptor.getFilterFlags(object);
if (filterFlags != null) {
for (String filterFlag : filterFlags) {
if (EXPERT_VIEW_FILTER_FLAG.equals(filterFlag)) {
return;
}
}
}
}
// Get the feature of the property fetch and its corresponding diffs multi-map.
Object feature = itemPropertyDescriptor.getFeature(object);
Multimap<Object, Diff> diffs = featureDiffs.remove(feature);
PropertyItem childItem = new PropertyDescriptorItem(configuration, object, diffs,
itemPropertyDescriptor, side);
// Fetch the map for the category, creating one if necessary.
String category = determineCategory(object, itemPropertyDescriptor);
Map<String, PropertyItem> items = categories.get(category);
if (items == null) {
items = Maps.newTreeMap();
categories.put(category, items);
}
// Put the item in the sorted map.
items.put(itemPropertyDescriptor.getDisplayName(object), childItem);
}
private static String determineCategory(Object object, IItemPropertyDescriptor itemPropertyDescriptor) {
// Determine the category, using misc if there isn't one.
String category = itemPropertyDescriptor.getCategory(object);
if (category == null) {
category = MISC_CATEGORY;
}
return category;
}
private static boolean shouldShowCategories(EMFCompareConfiguration configuration) {
return configuration.getBooleanProperty(PropertyContentMergeViewer.SHOW_CATEGORIES, true);
}
public static Match getMatch(EMFCompareConfiguration configuration, Object object) {
Match match = null;
if (object instanceof EObject) {
EObject eObject = (EObject)object;
match = configuration.getComparison().getMatch(eObject);
}
return match;
}
private static boolean shouldShowAdvancedProperties(EMFCompareConfiguration configuration) {
return configuration.getBooleanProperty(PropertyContentMergeViewer.SHOW_ADVANCED_PROPERTIES, false);
}
/**
* Builds map from each feature to a multi-map of each side value to its corresponding diff.
* <p>
* We can only do this if object is an {@link EObject} and if <code>match</code> isn't <code>null</code>.
* </p>
*
* @param object
* The object to build the featureToDiff map for.
* @param match
* the match of the object.
* @param comparison
* The comparison.
* @return map from each feature to a multi-map of each side value to its corresponding diff.
*/
private static Map<EStructuralFeature, Multimap<Object, Diff>> buildFeatureToDiffMap(Object object,
EMFCompareConfiguration configuration) {
final Match match = getMatch(configuration, object);
if (match == null || !(object instanceof EObject)) {
return Maps.newHashMap();
}
final Map<EStructuralFeature, Multimap<Object, Diff>> featureDiffs = Maps.newHashMap();
for (Diff diff : match.getDifferences()) {
// If that diff affects a specific feature...
EStructuralFeature eStructuralFeature = MergeViewerUtil.getAffectedFeature(diff);
if (eStructuralFeature != null) {
// Get the multi-map for that feature, creating a new one if necessary.
Multimap<Object, Diff> diffs = featureDiffs.get(eStructuralFeature);
if (diffs == null) {
diffs = HashMultimap.create();
featureDiffs.put(eStructuralFeature, diffs);
}
// Get the primary value of this diff and then iterate over the sides.
Object value = MatchUtil.getValue(diff);
for (MergeViewerSide valueSide : PropertyContentMergeViewer.MERGE_VIEWER_SIDES) {
// If there is a corresponding side value for the match...
EObject sideEObject = MergeViewerUtil.getEObject(match, valueSide);
if (sideEObject != null) {
// Get the corresponding value of that feature on that side.
List<Object> sideValues = ReferenceUtil.getAsList(sideEObject, eStructuralFeature);
// If the feature is multi-valued...
if (eStructuralFeature.isMany()) {
// Find the corresponding side-value of the value on those side values.
Object sideValue = MergeViewerUtil.matchingValue(value,
configuration.getComparison(), sideValues);
if (sideValue != null) {
diffs.put(sideValue, diff);
}
} else if (sideValues.isEmpty()) {
// Otherwise, directly use what's typically the one value in the side values.
diffs.put(null, diff);
} else {
diffs.put(sideValues.get(0), diff);
}
}
}
}
}
return featureDiffs;
}
/**
* Creates an instance of a property item.
*
* @param configuration
* the compare configuration.
* @param image
* the image of this property item.
* @param text
* the text of this property item.
* @param side
* the side of this property item.
*/
public PropertyItem(EMFCompareConfiguration configuration, Object image, String text,
MergeViewerSide side) {
super(text, image);
this.configuration = configuration;
this.side = side;
setSidePropertyItem(side, this);
}
/**
* Returns the corresponding property item for the specified side.
*
* @param anySide
* the side of the desired property item.
* @return the corresponding property item for the specified side.
*/
public PropertyItem getSide(MergeViewerSide anySide) {
switch (anySide) {
case ANCESTOR:
return ancestor;
case LEFT:
return left;
case RIGHT:
default:
return right;
}
}
/**
* This is called on a {@link #createPropertyItem(EMFCompareConfiguration, Object, MergeViewerSide) root}
* item by {@link PropertyContentMergeViewer#buildPropertiesFromSides(Object, Object, Object)} once it has
* built all three sides.
*
* @param newLeftSide
* the corresponding left-side root property item.
* @param newRightSide
* the corresponding right-side root property item.
*/
public void reconcile(PropertyItem newLeftSide, PropertyItem newRightSide) {
associate(MergeViewerSide.LEFT, newLeftSide);
associate(MergeViewerSide.RIGHT, newRightSide);
if (newLeftSide != null) {
newLeftSide.associate(MergeViewerSide.RIGHT, newRightSide);
reconcile(newLeftSide.getPropertyItems());
}
if (newRightSide != null) {
reconcile(newRightSide.getPropertyItems());
}
if (newLeftSide != null && newRightSide != null) {
newLeftSide.reconcile(newRightSide.getPropertyItems());
}
for (PropertyItem propertyItem : getPropertyItems()) {
propertyItem.reconcile();
}
if (newLeftSide != null) {
for (PropertyItem propertyItem : newLeftSide.getPropertyItems()) {
propertyItem.reconcile();
}
}
if (newRightSide != null) {
for (PropertyItem propertyItem : newRightSide.getPropertyItems()) {
propertyItem.reconcile();
}
}
}
/**
* Set the appropriate bidirectional side associations.
*
* @param otherSide
* the side of that other property item.
* @param propertyItem
* the other property item.
*/
private void associate(MergeViewerSide otherSide, PropertyItem propertyItem) {
setSidePropertyItem(side, this);
setSidePropertyItem(otherSide, propertyItem);
if (propertyItem != null) {
propertyItem.setSidePropertyItem(side, this);
propertyItem.setSidePropertyItem(otherSide, propertyItem);
}
}
/**
* Set the value of the appropriate side's field.
*
* @param otherSide
* the side to set.
* @param propertyItem
* the value to which to set it.
*/
private void setSidePropertyItem(MergeViewerSide otherSide, PropertyItem propertyItem) {
switch (otherSide) {
case ANCESTOR:
ancestor = propertyItem;
break;
case LEFT:
left = propertyItem;
break;
case RIGHT:
right = propertyItem;
break;
}
}
/**
* Reconcile's this side's properties against the other side property items.
*
* @param otherPropertyItems
* the other side's property items.
*/
private void reconcile(EList<PropertyItem> otherPropertyItems) {
EList<PropertyItem> propertyItems = getPropertyItems();
List<PropertyItem> remainingOtherPropertyItems = Lists.newArrayList(otherPropertyItems);
for (PropertyItem propertyItem : propertyItems) {
// This will associate the items, removing them once associated.
propertyItem.findMatchingItem(remainingOtherPropertyItems, true);
}
}
/**
* Reconcile the properties items of the sides against each other, and then recursively reconcile all the
* property items of each side.
*/
protected void reconcile() {
switch (side) {
case ANCESTOR:
if (left != null) {
reconcile(left.getPropertyItems());
}
if (right != null) {
reconcile(right.getPropertyItems());
}
break;
case LEFT:
if (right != null) {
left.reconcile(right.getPropertyItems());
}
break;
}
for (PropertyItem propertyItem : getPropertyItems()) {
propertyItem.reconcile();
}
}
/**
* Finds a matching item in the property items.
*
* @param propertyItem
* the item to find.
* @param propertyItems
* the items in which to find it.
* @param associate
* whether to associate the matching item and to remove it from the property items.
* @return the matching item.
*/
private PropertyItem findMatchingItem(List<? extends PropertyItem> propertyItems, boolean associate) {
for (PropertyItem otherPropertyItem : propertyItems) {
if (isMatchingItem(otherPropertyItem)) {
if (associate) {
associate(otherPropertyItem.side, otherPropertyItem);
propertyItems.remove(otherPropertyItem);
}
return otherPropertyItem;
}
}
return null;
}
/**
* Returns whether this property item matches the specified property item.
*
* @param propertyItem
* the property item against which to match.
* @return whether this property item matches the specified property item.
*/
protected abstract boolean isMatchingItem(PropertyItem propertyItem);
/**
* Determines if the two values {@link IEqualityHelper#matchingValues(Object, Object) match} using the
* comparison's equality helper.
*
* @param value1
* the first value.
* @param value2
* the second value.
* @return whether the two values match.
*/
protected boolean isMatchingValue(Object value1, Object value2) {
IEqualityHelper equalityHelper = configuration.getComparison().getEqualityHelper();
return equalityHelper.matchingValues(value1, value2);
}
/**
* Finds the corresponding property item of the specified property item somewhere within the receiver
* property item.
*
* @param propertyItem
* the property item to find.
* @return the corresponding property item or the deepest property item in the tree along the path of the
* specified property item.
*/
public PropertyItem findItem(PropertyItem propertyItem) {
PropertyItem propertyItemParent = propertyItem.getParent();
if (propertyItemParent == null) {
return this;
} else {
PropertyItem foundParent = findItem(propertyItemParent);
PropertyItem findMatchingItem = propertyItem.findMatchingItem(foundParent.getPropertyItems(),
false);
if (findMatchingItem == null) {
return this;
} else {
return findMatchingItem;
}
}
}
/**
* Returns the children, which must be property items.
*
* @return the children.
*/
@SuppressWarnings("unchecked")
public EList<PropertyItem> getPropertyItems() {
return (EList<PropertyItem>)(EList<?>)children;
}
protected boolean isModified() {
return false;
}
/**
* Returns the parent, which must be a property item.
*
* @return the parent.
*/
@Override
public PropertyItem getParent() {
return (PropertyItem)super.getParent();
}
/**
* Returns the primary object of this property item.
*
* @return the primary object of this property item.
*/
protected abstract Object getObject();
/**
* Returns the root property item.
*
* @return the root property item.
*/
public PropertyItem getRootItem() {
PropertyItem rootItem = this;
while (rootItem.getParent() != null) {
rootItem = rootItem.getParent();
}
return rootItem;
}
/**
* This must be called when the item property descriptor is expanded and collapsed. For lists it's
* designed to hide the property image and property text while the list is expanded, showing it again when
* it's collapsed.
*
* @param treeItem
* the item being expanded or collapsed.
* @param expanded
* whether the item is expanded as opposite to collapsed.
*/
public void update(TreeItem treeItem, boolean expanded) {
}
/**
* Returns the text for the value column of the property item.
*
* @return the text for the value column of the property item.
*/
protected String getPropertyText() {
return ""; //$NON-NLS-1$
}
/**
* Returns the image for the value column of the property item.
*
* @return the image for the value column of the property item.
*/
protected Object getPropertyImage() {
return null;
}
/**
* Returns the text for the property column or value column.
*
* @param object
* the object which is generally ignored.
* @param columnIndex
* either {@code 0} or {@code 1}, for the property column or value column respectively.
* @return the text for the property column or value column.
*/
public String getColumnText(Object object, int columnIndex) {
if (columnIndex == 0) {
return getText(object);
} else {
return getPropertyText();
}
}
/**
* Returns the image for the property column or value column.
*
* @param object
* the object which is generally ignored.
* @param columnIndex
* either {@code 0} or {@code 1}, for the property column or value column respectively.
* @return the image for the property column or value column.
*/
public Object getColumnImage(Object object, int columnIndex) {
if (columnIndex == 0) {
return getImage(object);
} else {
return getPropertyImage();
}
}
/**
* Returns the font for the property column or value column. {@link #isModified() Modified} property items
* will be shown in bold font.
*
* @param object
* the object, which is ignored.
* @param columnIndex
* either {@code 0} or {@code 1}, for the property column or value column respectively.
* @return the font for the property column or value column.
*/
public Object getFont(Object object, int columnIndex) {
if (isModified()) {
return IItemFontProvider.BOLD_FONT;
} else {
return null;
}
}
/**
* Returns the diff associated with this property item.
*
* @return the diff associated with this property item.
*/
@Override
public Diff getDiff() {
return null;
}
/**
* {@inheritDoc}
*/
public Object getLeft() {
if (left != null) {
return left.getObject();
}
return null;
}
/**
* {@inheritDoc}
*/
public Object getRight() {
if (right != null) {
return right.getObject();
}
return null;
}
/**
* {@inheritDoc}
*/
public Object getAncestor() {
if (ancestor != null) {
return ancestor.getObject();
}
return null;
}
/**
* {@inheritDoc}
*/
public Object getSideValue(MergeViewerSide anySide) {
switch (anySide) {
case ANCESTOR:
return getAncestor();
case LEFT:
return getLeft();
case RIGHT:
default:
return getRight();
}
}
/**
* {@inheritDoc}
*/
@Override
public MergeViewerSide getSide() {
return side;
}
/**
* {@inheritDoc}
*/
public boolean isInsertionPoint() {
return false;
}
/**
* {@inheritDoc}
*/
public void notifyChanged(Notification notification) {
}
/**
* {@inheritDoc}
*/
public Notifier getTarget() {
return null;
}
/**
* {@inheritDoc}
*/
public void setTarget(Notifier newTarget) {
}
/**
* {@inheritDoc}
*/
public boolean isAdapterForType(Object type) {
return false;
}
/**
* {@inheritDoc}
*/
public boolean hasChildren(IDifferenceGroupProvider group, Predicate<? super EObject> predicate) {
return false;
}
/**
* {@inheritDoc}
*/
public IMergeViewerItem[] getChildren(IDifferenceGroupProvider group,
Predicate<? super EObject> predicate) {
return null;
}
public EMFCompareConfiguration getConfiguration() {
return configuration;
}
}