/*******************************************************************************
* Copyright (c) 2012, 2016 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 static com.google.common.collect.Iterables.toArray;
import static com.google.common.collect.Iterables.transform;
import com.google.common.base.Function;
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.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.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.edit.tree.TreeNode;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.IContentProvider;
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 final 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 final Object[] getChildren(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);
}
final Object[] compareInputChildren;
// Avoid NPE.
if (children == null) {
children = new Object[] {};
}
// Do not adapt if it's a pending updater
if (!Iterables.all(Arrays.asList(children), Predicates.instanceOf(PendingUpdateAdapter.class))) {
Iterable<?> compareInputs = adapt(children, getAdapterFactory(), ICompareInput.class);
compareInputChildren = toArray(compareInputs, Object.class);
} else {
compareInputChildren = children;
}
return compareInputChildren;
}
/**
* 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();
}
/**
* Adapts each elements of the the given <code>iterable</code> to the given <code>type</code> by using the
* given <code>adapterFactory</code>.
*
* @param <T>
* the type of returned elements.
* @param iterable
* the iterable to transform.
* @param adapterFactory
* the {@link AdapterFactory} used to adapt elements.
* @param type
* the target type of adapted elements.
* @return an iterable with element of type <code>type</code>.
*/
private static Iterable<?> adapt(Iterable<?> iterable, final AdapterFactory adapterFactory,
final Class<?> type) {
Function<Object, Object> adaptFunction = new Function<Object, Object>() {
public Object apply(Object input) {
Object ret = adapterFactory.adapt(input, type);
if (ret == null) {
return input;
}
return ret;
}
};
return transform(iterable, adaptFunction);
}
/**
* Adapts each elements of the the given <code>array</code> to the given <code>type</code> by using the
* given <code>adapterFactory</code>.
*
* @param <T>
* the type of returned elements.
* @param iterable
* the array to transform.
* @param adapterFactory
* the {@link AdapterFactory} used to adapt elements.
* @param type
* the target type of adapted elements
* @return an iterable with element of type <code>type</code>.
*/
private static Iterable<?> adapt(Object[] iterable, final AdapterFactory adapterFactory,
final Class<?> type) {
return adapt(Arrays.asList(iterable), adapterFactory, type);
}
/**
* {@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
}
}