blob: 8a13dc10bc6a928c4c1bcebf3a64e6178668358c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2017 Obeo 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:
* Obeo - initial API and implementation
* Michael Borkowski - public CallbackType visibility for testing
* Stefan Dirix - bug 473985
*******************************************************************************/
package org.eclipse.emf.compare.ide.ui.internal.structuremergeviewer;
import static com.google.common.base.Predicates.instanceOf;
import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.IJobChangeListener;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIMessages;
import org.eclipse.emf.compare.ide.ui.internal.treecontentmanager.EMFCompareDeferredTreeContentManager;
import org.eclipse.emf.compare.ide.ui.internal.treecontentmanager.EMFCompareDeferredTreeContentManagerUtil;
import org.eclipse.emf.compare.rcp.ui.internal.util.SWTUtil;
import org.eclipse.emf.compare.rcp.ui.structuremergeviewer.groups.IDifferenceGroupProvider2;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.provider.IViewerNotification;
import org.eclipse.emf.edit.provider.IWrapperItemProvider;
import org.eclipse.emf.edit.provider.ViewerNotification;
import org.eclipse.emf.edit.tree.TreeNode;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import org.eclipse.emf.edit.ui.provider.NotifyChangedToViewerRefresh;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.ui.progress.DeferredTreeContentManager;
import org.eclipse.ui.progress.IDeferredWorkbenchAdapter;
import org.eclipse.ui.progress.IElementCollector;
import org.eclipse.ui.progress.PendingUpdateAdapter;
/**
* Specialized AdapterFactoryContentProvider for the emf compare structure merge viewer.
* <p>
* <i>This class is not intended to be used outside of its package. It has been set to public for testing
* purpose only.</i>
* </p>
*
* @author <a href="mailto:mikael.barbero@obeo.fr">Mikael Barbero</a>
*/
public class EMFCompareStructureMergeViewerContentProvider extends AdapterFactoryContentProvider implements IJobChangeListener {
/**
* Class to listen the state of the content provider.
*
* @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
*/
static class FetchListener {
/**
* This method is called when the content provider starts fetching new elements in an external Job.
*/
public void startFetching() {
}
/**
* This method is called when the content provider has finished fetching elements and has removed the
* {@link PendingUpdateAdapter} from the tree.
*/
public void doneFetching() {
}
}
/**
* Callback holder used to defer the run of a callback in a specific thread.
*
* @see {@link #callback}
* @see EMFCompareStructureMergeViewerContentProvider#runWhenReady(CallbackType, Runnable)
*/
private static class CallbackHolder {
private final Runnable callback;
private final CallbackType callbackType;
public CallbackHolder(Runnable callback, CallbackType callbackType) {
super();
this.callback = callback;
this.callbackType = callbackType;
}
private Runnable getCallback() {
return callback;
}
private CallbackType getType() {
return callbackType;
}
}
/**
* {@link IDeferredWorkbenchAdapter} using this content provider to fetch children.
*
* @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
*/
private static class EMFCompareStructureMergeViewerContentProviderDeferredAdapter implements IDeferredWorkbenchAdapter {
private final EMFCompareStructureMergeViewerContentProvider contentProvider;
public EMFCompareStructureMergeViewerContentProviderDeferredAdapter(
EMFCompareStructureMergeViewerContentProvider contentProvider) {
super();
this.contentProvider = contentProvider;
}
/**
* {@inheritDoc}
*
* @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#getChildren(Object)}
*/
public Object[] getChildren(Object o) {
// Not used
return null;
}
/**
* {@inheritDoc}
*
* @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#getImageDescriptor(Object)}
*/
public ImageDescriptor getImageDescriptor(Object object) {
// Not used
return null;
}
/**
* {@inheritDoc}
*
* @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#getLabel(Object)}
*/
public String getLabel(Object o) {
return EMFCompareIDEUIMessages.getString(
"EMFCompareStructureMergeViewerContentProvider.deferredWorkbenchAdapter.label"); //$NON-NLS-1$
}
/**
* {@inheritDoc}
*
* @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#getParent(Object)}
*/
public Object getParent(Object o) {
// Not used
return null;
}
/**
* {@inheritDoc}
*
* @see {IDeferredWorkbenchAdapter
* {@link IDeferredWorkbenchAdapter#fetchDeferredChildren(Object object, IElementCollector collector, IProgressMonitor monitor)}
*/
public void fetchDeferredChildren(Object object, IElementCollector collector,
IProgressMonitor monitor) {
if (!monitor.isCanceled()) {
if (object instanceof CompareInputAdapter) {
Notifier target = ((Adapter)object).getTarget();
Object[] children = contentProvider.getChildren(target);
collector.add(children, monitor);
}
}
}
/**
* {@inheritDoc}
*
* @see {IDeferredWorkbenchAdapter{@link IDeferredWorkbenchAdapter#isContainer()}
*/
public boolean isContainer() {
// Not used
return true;
}
/**
* {@inheritDoc}
*
* @see {IDeferredWorkbenchAdapter{@link #getRule(Object)}
*/
public ISchedulingRule getRule(Object object) {
// Not used
return null;
}
}
/** {@link DeferredTreeContentManager} use to fetch groups in a external {@link Job}. */
private final EMFCompareDeferredTreeContentManager contentManagerAdapter;
/** Holds true if this content provider is currently fetching children. */
private boolean isFetchingGroup;
/** Will protect R/W of {@link #pending} and {@link #isFetchingGroup}. */
private final ReentrantLock lock;
/** Object listening the status of this object. */
private final List<FetchListener> listeners;
/** List of current callbacks. Callbacks are only run once. */
private List<CallbackHolder> callbacks;
/** Pending object displayed in the tree. */
private Object[] pending;
/**
* Constructs the content provider with the appropriate adapter factory.
*
* @param adapterFactory
* The adapter factory used to construct the content provider.
*/
public EMFCompareStructureMergeViewerContentProvider(AdapterFactory adapterFactory,
AbstractTreeViewer viewer) {
super(adapterFactory);
contentManagerAdapter = EMFCompareDeferredTreeContentManagerUtil
.createEMFDeferredTreeContentManager(viewer);
contentManagerAdapter.addUpdateCompleteListener(this);
lock = new ReentrantLock();
listeners = new CopyOnWriteArrayList<EMFCompareStructureMergeViewerContentProvider.FetchListener>();
callbacks = new CopyOnWriteArrayList<EMFCompareStructureMergeViewerContentProvider.CallbackHolder>();
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider#getParent(Object object)
*/
@Override
public Object getParent(Object element) {
final Object ret;
if (element instanceof CompareInputAdapter) {
Object parentNode = super.getParent(((Adapter)element).getTarget());
if (parentNode instanceof TreeNode) {
final Optional<Adapter> cia = Iterators.tryFind(((TreeNode)parentNode).eAdapters().iterator(),
instanceOf(CompareInputAdapter.class));
if (cia.isPresent()) {
ret = cia.get();
} else {
ret = parentNode;
}
} else {
ret = parentNode;
}
} else if (element instanceof ICompareInput) {
ret = null;
} else {
ret = super.getParent(element);
}
return ret;
}
/**
* Enum used for better readability of the method
* {@link EMFCompareStructureMergeViewerContentProvider#runWhenReady(CallbackType, Runnable)}.
*
* @author <a href="mailto:arthur.daussy@obeo.fr">Arthur Daussy</a>
*/
// public for testing
public static enum CallbackType {
/** Run the runnable in the UI thread synchronously. */
IN_UI_SYNC,
/** Run the runnable in the UI thread asynchronously. */
IN_UI_ASYNC,
/** Run the runnable in the current thread. */
IN_CURRENT_THREAD
}
/**
* Run the given runnable in the specified thread when then content provider is ready. It can be run
* directly if the content provider is not fecthing or during a callback when the content provider is done
* fetching.
*
* @param type
* of thread to run the {@link Runnable} inside.
* @param runnable
* to run
*/
public void runWhenReady(CallbackType type, final Runnable runnable) {
// Prevents adding a callback if another thread set this content provider as not fetching.
lock.lock();
try {
if (isFetchingGroup) {
callbacks.add(new CallbackHolder(runnable, type));
} else {
run(runnable, type);
}
} finally {
lock.unlock();
}
}
/**
* Runs a callback in the related thread.
*
* @param callback
* to run.
* @param type
* of thread.
*/
private void run(Runnable callback, CallbackType type) {
switch (type) {
case IN_UI_SYNC:
SWTUtil.safeSyncExec(callback);
break;
case IN_UI_ASYNC:
SWTUtil.safeAsyncExec(callback);
break;
default:
callback.run();
break;
}
}
/**
* Adds a listener to this content provider.
*
* @param listener
* to add
* @return
*/
public boolean addFetchingListener(FetchListener listener) {
return listeners.add(listener);
}
/**
* Removes a listener to this content provider.
*
* @param listener
* to remove
* @return
*/
public boolean removeFetchingListener(FetchListener listener) {
return listeners.remove(listener);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider#hasChildren(Object object)
*/
@Override
public boolean hasChildren(Object element) {
final boolean ret;
if (element instanceof CompareInputAdapter) {
ret = super.hasChildren(((Adapter)element).getTarget());
} else if (element instanceof ICompareInput) {
ret = false;
} else {
ret = super.hasChildren(element);
}
return ret;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider#getChildren(java.lang.Object)
*/
@Override
public Object[] getChildren(Object element) {
Object[] rawChildren = getRawChildren(element);
// Adapt all children that are not PendingUpdateAdapters into ICompareInput elements
return adaptElementsToCompareInput(rawChildren);
}
/**
* Returns the raw list of children for the given element. This array will not be useable by the EMF
* Compare viewers without prior going through {@link #adaptElementsToCompareInput(Object[])}.
*
* @param element
* The element which children we need.
* @return The children of this element.
*/
protected Object[] getRawChildren(Object element) {
Object[] children;
if (element instanceof CompareInputAdapter) {
children = getCompareInputAdapterChildren((CompareInputAdapter)element);
} else if (element instanceof ICompareInput) {
children = new Object[] {};
} else {
children = super.getChildren(element);
}
// Avoid NPE.
if (children == null) {
children = new Object[] {};
}
return children;
}
/**
* Adapt all of the given elements that are not instances of {@link PendingUpdateAdapter} into
* {@link ICompareInput} for the emf compare viewers when possible.
*
* @param elements
* The elements to adapt.
* @return The array of adapted elements.
*/
protected Object[] adaptElementsToCompareInput(Object[] elements) {
Object[] result = new Object[elements.length];
for (int i = 0; i < result.length; i++) {
Object child = elements[i];
if (child instanceof PendingUpdateAdapter) {
result[i] = child;
} else {
result[i] = adapterFactory.adapt(child, ICompareInput.class);
if (result[i] == null) {
result[i] = child;
}
}
}
return result;
}
/**
* Returns a {@link PendingUpdateAdapter} while a Job is fetching the children for this object or the
* children if they have already been fetched.
* <p>
* When the job is finished it will autamically replace the {@link PendingUpdateAdapter} by the fetched
* children. The fetched children will be stored under the TreeItem holding the input object or at the
* root of the tree if the input object match the input of the tree viewer.
* </p>
*
* @param compareInputAdapter
* @return
*/
private Object[] getCompareInputAdapterChildren(CompareInputAdapter compareInputAdapter) {
Notifier target = compareInputAdapter.getTarget();
if (target instanceof TreeNode) {
TreeNode treeNode = (TreeNode)target;
if (treeNode.getData() instanceof Comparison) {
IDifferenceGroupProvider2 groupProvider2 = getGroupProvider2(treeNode);
// Handles the first initialisation of the groups.
lock.lock();
try {
if (groupProvider2 != null && !groupProvider2.groupsAreBuilt()) {
return deferReturnChildren(compareInputAdapter);
}
} finally {
lock.unlock();
}
}
}
return super.getChildren(compareInputAdapter.getTarget());
}
private Object[] deferReturnChildren(CompareInputAdapter compareInputAdapter) {
if (!isFetchingGroup) {
isFetchingGroup = true;
/*
* Notifies listeners that the content provider starts fetching here and not in
* EMFCompareStructureMergeViewerContentProvider#aboutToRun() since it is only notified on the
* "clear pending updater" job events and not on the "fetching children" job events.
* @see org.eclipse.ui.progress.DeferredTreeContentManager.runClearPlaceholderJob(
* PendingUpdateAdapter)
*/
for (FetchListener callback : listeners) {
callback.startFetching();
}
compareInputAdapter.setDeferredAdapter(
new EMFCompareStructureMergeViewerContentProviderDeferredAdapter(this));
pending = contentManagerAdapter.getChildren(compareInputAdapter);
}
return pending;
}
private IDifferenceGroupProvider2 getGroupProvider2(TreeNode treeNode) {
IDifferenceGroupProvider2 result = null;
Optional<Adapter> searchResult = Iterables.tryFind(treeNode.eAdapters(),
Predicates.instanceOf(IDifferenceGroupProvider2.class));
if (searchResult.isPresent()) {
return (IDifferenceGroupProvider2)searchResult.get();
}
return result;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider#getElements(Object object)
*/
@Override
public Object[] getElements(Object element) {
return getChildren(element);
}
/**
* {@inheritDoc}
*
* @see IContentProvider#dispose()
*/
@Override
public void dispose() {
super.dispose();
contentManagerAdapter.removeUpdateCompleteListener(this);
listeners.clear();
}
/**
* This implementation specializes the EMF implementation to ensure that if more than 30 notifications
* arrive, the viewer is simply refreshed rather than processing each update separately. This is
* especially important for when undo is invoked in editor and there are many things to be undone.
* <p/>
* {@inheritDoc}
*
* @see IContentProvider#dispose()
*/
@Override
public void notifyChanged(Notification notification) {
if (viewer != null && viewer.getControl() != null && !viewer.getControl().isDisposed()
&& viewer.getInput() != null) {
if (notification instanceof IViewerNotification) {
if (viewerRefresh == null) {
viewerRefresh = new EMFCompareViewerRefresh(viewer);
}
if (viewerRefresh.addNotification((IViewerNotification)notification)) {
viewer.getControl().getDisplay().asyncExec(viewerRefresh);
}
} else {
NotifyChangedToViewerRefresh.handleNotifyChanged(viewer, notification.getNotifier(),
notification.getEventType(), notification.getFeature(), notification.getOldValue(),
notification.getNewValue(), notification.getPosition());
}
}
}
/**
* {@inheritDoc}
*
* @see IJobChangeListener{@link #aboutToRun(IJobChangeEvent)
*/
public void aboutToRun(IJobChangeEvent event) {
/*
* Nothing to do here since it has already been done in
* EMFCompareStructureMergeViewerContentProvider#getCompareInputAdapterChildren(CompareInputAdapter
* compareInputAdapter)
*/
}
/**
* {@inheritDoc}
*
* @see IJobChangeListener#awake(IJobChangeEvent)
*/
public void awake(IJobChangeEvent event) {
// Nothing to do
}
/**
* {@inheritDoc}
*
* @see IJobChangeListener#done(IJobChangeEvent)
*/
public void done(IJobChangeEvent event) {
if (event.getResult().isOK()) {
// Prevents running callbacks while another thread add a new callback or another thread launch a
// fetching job.
lock.lock();
try {
if (isFetchingGroup) {
isFetchingGroup = false;
pending = null;
for (FetchListener listener : listeners) {
listener.doneFetching();
// If the listener starts to fetch again then stop notifying listeners and wait for
// the content provider to be ready before re-starting.
if (isFetchingGroup) {
return;
}
}
final Iterator<CallbackHolder> callbacksIterator = callbacks.iterator();
while (callbacksIterator.hasNext()) {
CallbackHolder callbackHolder = callbacksIterator.next();
run(callbackHolder.getCallback(), callbackHolder.getType());
// If the callback has started to fetch again the stop running callbacks and wait for
// the content provider to be ready.
if (isFetchingGroup) {
List<CallbackHolder> remainingCallBack = Lists.newArrayList(callbacksIterator);
callbacks = new CopyOnWriteArrayList<EMFCompareStructureMergeViewerContentProvider.CallbackHolder>(
remainingCallBack);
return;
}
}
callbacks.clear();
}
} finally {
lock.unlock();
}
}
}
/**
* {@inheritDoc}
*
* @see IJobChangeListener#running(IJobChangeEvent)
*/
public void running(IJobChangeEvent event) {
// Nothing to do
}
/**
* {@inheritDoc}
*
* @see IJobChangeListener#scheduled(IJobChangeEvent)
*/
public void scheduled(IJobChangeEvent event) {
// Nothing to do
}
/**
* {@inheritDoc}
*
* @see IJobChangeListener#sleeping(IJobChangeEvent)
*/
public void sleeping(IJobChangeEvent event) {
// Nothing to do
}
/**
* This custom implementation of a ViewerRefresh intends to suppress the thousands of
* NoSuchMethodExceptions thrown by the regular implementation when it calls
* {@link AdapterFactoryEditingDomain#isStale(Object)}.
*/
private static class EMFCompareViewerRefresh extends ViewerRefresh {
int count = 0;
final Viewer targetViewer;
public EMFCompareViewerRefresh(Viewer viewer) {
super(viewer);
targetViewer = viewer;
}
/**
* Copy/paste of the super implementation except for the
* {@link AdapterFactoryEditingDomain#isStale(Object)} calls which are replaced with calls to
* {@link #isStale(Object)}.
*/
@Override
protected void refresh(IViewerNotification notification) {
Object element = notification.getElement();
if (targetViewer instanceof StructuredViewer) {
StructuredViewer structuredViewer = (StructuredViewer)targetViewer;
ISelection selection = structuredViewer.getSelection();
boolean isStaleSelection = isStale(selection);
if (isStaleSelection) {
targetViewer.setSelection(StructuredSelection.EMPTY);
}
AbstractTreeViewer treeViewer = structuredViewer instanceof AbstractTreeViewer
? (AbstractTreeViewer)structuredViewer
: null;
List<Object> expandedElements = treeViewer == null ? Collections.emptyList()
: Arrays.asList(treeViewer.getExpandedElements());
boolean isStaleExpandedElements = isStale(expandedElements);
if (element != null) {
if (notification.isContentRefresh()) {
structuredViewer.refresh(element, notification.isLabelUpdate());
} else if (notification.isLabelUpdate()) {
structuredViewer.update(element, null);
}
} else {
structuredViewer.refresh(notification.isLabelUpdate());
}
if (isStaleSelection || isStaleExpandedElements) {
Object object = structuredViewer.getInput();
EditingDomain editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(object);
if (editingDomain == null) {
for (Object child : ((IStructuredContentProvider)structuredViewer
.getContentProvider()).getElements(object)) {
editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(child);
if (editingDomain != null) {
break;
}
}
}
if (editingDomain instanceof AdapterFactoryEditingDomain) {
AdapterFactoryEditingDomain adapterFactoryEditingDomain = (AdapterFactoryEditingDomain)editingDomain;
if (treeViewer != null && isStaleExpandedElements) {
treeViewer.setExpandedElements(
adapterFactoryEditingDomain.resolve(expandedElements).toArray());
}
if (isStaleSelection) {
structuredViewer
.setSelection(
new StructuredSelection(adapterFactoryEditingDomain
.resolve(((IStructuredSelection)selection).toList())),
true);
}
}
}
} else {
targetViewer.refresh();
}
}
/**
* Copy/paste of {@link AdapterFactoryEditingDomain#isStale(Object)} but prevents exceptions from
* being thrown.
*/
private boolean isStale(Object object) {
if (object instanceof IWrapperItemProvider) {
IWrapperItemProvider wrapper = (IWrapperItemProvider)object;
return isStale(wrapper.getValue()) || isStale(wrapper.getOwner());
} else if (object instanceof Collection<?>) {
for (Object item : (Collection<?>)object) {
if (isStale(item)) {
return true;
}
}
return false;
} else if (object instanceof Object[]) {
for (Object item : (Object[])object) {
if (isStale(item)) {
return true;
}
}
return false;
} else if (object instanceof EObject) {
EObject eObject = (EObject)object;
return eObject.eIsProxy() && eObject.eAdapters().isEmpty();
} else if (object instanceof FeatureMap.Entry) {
return isStale(((FeatureMap.Entry)object).getValue());
} else if (object == null) {
return false;
} else if (object instanceof IStructuredSelection) {
return isStale(((IStructuredSelection)object).toArray());
} else {
return false;
}
}
@Override
public synchronized boolean addNotification(IViewerNotification notification) {
if (super.addNotification(notification)) {
count = 0;
return true;
}
// When there are more than 30 notifications, it's probably cheaper to simply
// refresh the overall view.
if (count > 30) {
super.addNotification(new ViewerNotification(notification, null, true, true));
}
++count;
return false;
}
}
}