blob: bcc9d7c62ceb992a829dc8efad2cc689ded05735 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
*
* 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.team.internal.ui.synchronize;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Control;
import org.eclipse.team.core.ITeamStatus;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.core.synchronize.ISyncInfoSetChangeEvent;
import org.eclipse.team.core.synchronize.ISyncInfoSetChangeListener;
import org.eclipse.team.core.synchronize.ISyncInfoTreeChangeEvent;
import org.eclipse.team.core.synchronize.SyncInfoSet;
import org.eclipse.team.internal.core.BackgroundEventHandler;
import org.eclipse.team.internal.ui.Policy;
import org.eclipse.team.internal.ui.TeamUIMessages;
import org.eclipse.team.internal.ui.TeamUIPlugin;
import org.eclipse.team.internal.ui.Utils;
import org.eclipse.team.ui.synchronize.ISynchronizeModelElement;
/**
* Handler that serializes the updating of a synchronize model provider.
* All modifications to the synchronize model are performed in this
* handler's thread.
*/
public class SynchronizeModelUpdateHandler extends BackgroundEventHandler implements IResourceChangeListener, ISyncInfoSetChangeListener {
private static final IWorkspaceRoot ROOT = ResourcesPlugin.getWorkspace().getRoot();
// Event that indicates that the markers for a set of elements has changed
private static final int MARKERS_CHANGED = 1;
private static final int BUSY_STATE_CHANGED = 2;
private static final int RESET = 3;
private static final int SYNC_INFO_SET_CHANGED = 4;
private AbstractSynchronizeModelProvider provider;
private Set<ISynchronizeModelElement> pendingLabelUpdates = Collections.synchronizedSet(new HashSet<>());
// Flag to indicate the need for an early dispath in order to show
// busy for elements involved in an operation
private boolean dispatchEarly = false;
private static final int EARLY_DISPATCH_INCREMENT = 100;
/**
* Custom event for posting marker changes
*/
static class MarkerChangeEvent extends Event {
private final ISynchronizeModelElement[] elements;
public MarkerChangeEvent(ISynchronizeModelElement[] elements) {
super(MARKERS_CHANGED);
this.elements = elements;
}
public ISynchronizeModelElement[] getElements() {
return elements;
}
}
/**
* Custom event for posting busy state changes
*/
static class BusyStateChangeEvent extends Event {
private final ISynchronizeModelElement element;
private final boolean isBusy;
public BusyStateChangeEvent(ISynchronizeModelElement element, boolean isBusy) {
super(BUSY_STATE_CHANGED);
this.element = element;
this.isBusy = isBusy;
}
public ISynchronizeModelElement getElement() {
return element;
}
public boolean isBusy() {
return isBusy;
}
}
/**
* Custom event for posting sync info set changes
*/
static class SyncInfoSetChangeEvent extends Event {
private final ISyncInfoSetChangeEvent event;
public SyncInfoSetChangeEvent(ISyncInfoSetChangeEvent event) {
super(SYNC_INFO_SET_CHANGED);
this.event = event;
}
public ISyncInfoSetChangeEvent getEvent() {
return event;
}
}
private IPropertyChangeListener listener = event -> {
if (event.getProperty() == ISynchronizeModelElement.BUSY_PROPERTY) {
Object source = event.getSource();
if (source instanceof ISynchronizeModelElement)
updateBusyState((ISynchronizeModelElement)source, ((Boolean)event.getNewValue()).booleanValue());
}
};
private boolean performingBackgroundUpdate;
/*
* Map used to keep track of additions so they can be added in batch at the end of the update
*/
private Map<ISynchronizeModelElement, Set<ISynchronizeModelElement>> additionsMap;
/**
* Create the marker update handler.
*/
public SynchronizeModelUpdateHandler(AbstractSynchronizeModelProvider provider) {
super(TeamUIMessages.SynchronizeModelProvider_0, TeamUIMessages.SynchronizeModelUpdateHandler_0); //
this.provider = provider;
ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
provider.getSyncInfoSet().addSyncSetChangedListener(this);
}
/**
* Return the marker types that are of interest to this handler.
* @return the marker types that are of interest to this handler
*/
protected String[] getMarkerTypes() {
return new String[] {IMarker.PROBLEM};
}
/**
* Return the <code>AbstractTreeViewer</code> associated with this
* provider or <code>null</code> if the viewer is not of the proper type.
* @return the structured viewer that is displaying the model managed by this provider
*/
public StructuredViewer getViewer() {
return provider.getViewer();
}
@Override
public void resourceChanged(final IResourceChangeEvent event) {
String[] markerTypes = getMarkerTypes();
Set<IResource> handledResources = new HashSet<>();
Set<ISynchronizeModelElement> changes = new HashSet<>();
// Accumulate all distinct resources that have had problem marker
// changes
for (String markerType : markerTypes) {
IMarkerDelta[] markerDeltas = event.findMarkerDeltas(markerType, true);
for (IMarkerDelta delta : markerDeltas) {
IResource resource = delta.getResource();
if (!handledResources.contains(resource)) {
handledResources.add(resource);
ISynchronizeModelElement[] elements = provider.getClosestExistingParents(delta.getResource());
if (elements != null && elements.length > 0) {
Collections.addAll(changes, elements);
}
}
}
}
if (!changes.isEmpty()) {
updateMarkersFor(changes.toArray(new ISynchronizeModelElement[changes.size()]));
}
}
private void updateMarkersFor(ISynchronizeModelElement[] elements) {
queueEvent(new MarkerChangeEvent(elements), false /* not on front of queue */);
}
protected void updateBusyState(ISynchronizeModelElement element, boolean isBusy) {
queueEvent(new BusyStateChangeEvent(element, isBusy), false /* not on front of queue */);
}
@Override
protected void processEvent(Event event, IProgressMonitor monitor) throws CoreException {
switch (event.getType()) {
case BackgroundEventHandler.RUNNABLE_EVENT :
executeRunnable(event, monitor);
break;
case MARKERS_CHANGED:
// Changes contains all elements that need their labels updated
long start = System.currentTimeMillis();
ISynchronizeModelElement[] elements = getChangedElements(event);
for (ISynchronizeModelElement element : elements) {
propagateProblemMarkers(element);
updateParentLabels(element);
}
if (Policy.DEBUG_SYNC_MODELS) {
long time = System.currentTimeMillis() - start;
DateFormat TIME_FORMAT = new SimpleDateFormat("m:ss.SSS"); //$NON-NLS-1$
String took = TIME_FORMAT.format(new Date(time));
System.out.println(took + " for " + elements.length + " files"); //$NON-NLS-1$//$NON-NLS-2$
}
break;
case BUSY_STATE_CHANGED:
BusyStateChangeEvent e = (BusyStateChangeEvent)event;
queueForLabelUpdate(e.getElement());
if (e.isBusy()) {
// indicate that we want an early dispatch to show busy elements
dispatchEarly = true;
}
break;
case RESET:
// Perform the reset immediately
pendingLabelUpdates.clear();
provider.reset();
break;
case SYNC_INFO_SET_CHANGED:
// Handle the sync change immediately
handleChanges(((SyncInfoSetChangeEvent)event).getEvent(), monitor);
default:
break;
}
}
private ISynchronizeModelElement[] getChangedElements(Event event) {
if (event.getType() == MARKERS_CHANGED) {
return ((MarkerChangeEvent)event).getElements();
}
return new ISynchronizeModelElement[0];
}
@Override
protected boolean doDispatchEvents(IProgressMonitor monitor) throws TeamException {
// Fire label changed
dispatchEarly = false;
if (pendingLabelUpdates.isEmpty()) {
return false;
} else {
Utils.asyncExec((Runnable) this::firePendingLabelUpdates, getViewer());
return true;
}
}
/**
* Forces the viewer to update the labels for queued elemens
* whose label has changed during this round of changes. This method
* should only be invoked in the UI thread.
*/
protected void firePendingLabelUpdates() {
if (!Utils.canUpdateViewer(getViewer())) return;
try {
Object[] updates = pendingLabelUpdates.toArray(new Object[pendingLabelUpdates.size()]);
updateLabels(updates);
} finally {
pendingLabelUpdates.clear();
}
}
/*
* Forces the viewer to update the labels for the given elements
*/
private void updateLabels(Object[] elements) {
StructuredViewer tree = getViewer();
if (Utils.canUpdateViewer(tree)) {
tree.update(elements, null);
}
}
/**
* Queue all the parent elements for a label update.
* @param element the element whose label and parent labels need to be updated
*/
public void updateParentLabels(ISynchronizeModelElement element) {
queueForLabelUpdate(element);
while (element.getParent() != null) {
element = (ISynchronizeModelElement)element.getParent();
queueForLabelUpdate(element);
}
}
/**
* Update the label of the given diff node. Diff nodes
* are accumulated and updated in a single call.
* @param diffNode the diff node to be updated
*/
protected void queueForLabelUpdate(ISynchronizeModelElement diffNode) {
pendingLabelUpdates.add(diffNode);
}
/**
* Calculate and propagate problem markers in the element model
* @param element the ssynchronize element
*/
private void propagateProblemMarkers(ISynchronizeModelElement element) {
IResource resource = element.getResource();
if (resource != null) {
String property = provider.calculateProblemMarker(element);
// If it doesn't have a direct change, a parent might
boolean recalculateParentDecorations = hadProblemProperty(element, property);
if (recalculateParentDecorations) {
ISynchronizeModelElement parent = (ISynchronizeModelElement) element.getParent();
if (parent != null) {
propagateProblemMarkers(parent);
}
}
}
}
// none -> error
// error -> none
// none -> warning
// warning -> none
// warning -> error
// error -> warning
private boolean hadProblemProperty(ISynchronizeModelElement element, String property) {
boolean hadError = element.getProperty(ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY);
boolean hadWarning = element.getProperty(ISynchronizeModelElement.PROPAGATED_WARNING_MARKER_PROPERTY);
// Force recalculation of parents of phantom resources
IResource resource = element.getResource();
if(resource != null && resource.isPhantom()) {
return true;
}
if(hadError) {
if(! (property == ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY)) {
element.setPropertyToRoot(ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY, false);
if(property != null) {
// error -> warning
element.setPropertyToRoot(property, true);
}
// error -> none
// recalculate parents
return true;
}
return false;
} else if(hadWarning) {
if(! (property == ISynchronizeModelElement.PROPAGATED_WARNING_MARKER_PROPERTY)) {
element.setPropertyToRoot(ISynchronizeModelElement.PROPAGATED_WARNING_MARKER_PROPERTY, false);
if(property != null) {
// warning -> error
element.setPropertyToRoot(property, true);
return false;
}
// warning -> none
return true;
}
return false;
} else {
if(property == ISynchronizeModelElement.PROPAGATED_ERROR_MARKER_PROPERTY) {
// none -> error
element.setPropertyToRoot(property, true);
return false;
} else if(property == ISynchronizeModelElement.PROPAGATED_WARNING_MARKER_PROPERTY) {
// none -> warning
element.setPropertyToRoot(property, true);
return true;
}
return false;
}
}
/*
* Queue an event that will reset the provider
*/
private void reset() {
queueEvent(new ResourceEvent(ROOT, RESET, IResource.DEPTH_INFINITE), false);
}
public void dispose() {
shutdown();
ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
provider.getSyncInfoSet().removeSyncSetChangedListener(this);
}
@Override
protected long getShortDispatchDelay() {
if (dispatchEarly) {
dispatchEarly = false;
return EARLY_DISPATCH_INCREMENT;
}
return super.getShortDispatchDelay();
}
/**
* This method is invoked whenever a node is added to the viewer
* by the provider or a sub-provider. The handler adds an update
* listener to the node and notifies the root provider that
* a node was added.
* @param element the added element
* @param provider the provider that added the element
*/
public void nodeAdded(ISynchronizeModelElement element, AbstractSynchronizeModelProvider provider) {
element.addPropertyChangeListener(listener);
this.provider.nodeAdded(element, provider);
if (Policy.DEBUG_SYNC_MODELS) {
System.out.println("Node added: " + getDebugDisplayLabel(element) + " -> " + getDebugDisplayLabel((ISynchronizeModelElement)element.getParent()) + " : " + getDebugDisplayLabel(provider)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
/**
* This method is invoked whenever a node is removed the viewer
* by the provider or a sub-provider. The handler removes any
* listener and notifies the root provider that
* a node was removed. The node removed may have children for which
* a nodeRemoved callback was not invoked (see modelObjectCleared).
* @param element the removed element
* @param provider the provider that added the element
*/
public void nodeRemoved(ISynchronizeModelElement element, AbstractSynchronizeModelProvider provider) {
element.removePropertyChangeListener(listener);
this.provider.nodeRemoved(element, provider);
if (Policy.DEBUG_SYNC_MODELS) {
System.out.println("Node removed: " + getDebugDisplayLabel(element) + " -> " + getDebugDisplayLabel((ISynchronizeModelElement)element.getParent()) + " : " + getDebugDisplayLabel(provider)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
/**
* This method is invoked whenever a model object (i.e. node)
* is cleared from the model. This is similar to node removal but
* is deep.
* @param node the node that was cleared
*/
public void modelObjectCleared(ISynchronizeModelElement node) {
node.removePropertyChangeListener(listener);
this.provider.modelObjectCleared(node);
if (Policy.DEBUG_SYNC_MODELS) {
System.out.println("Node cleared: " + getDebugDisplayLabel(node)); //$NON-NLS-1$
}
}
private String getDebugDisplayLabel(ISynchronizeModelElement node) {
if (node == null) {
return "ROOT"; //$NON-NLS-1$
}
if (node.getResource() != null) {
return node.getResource().getFullPath().toString();
}
return node.getName();
}
private String getDebugDisplayLabel(AbstractSynchronizeModelProvider provider2) {
return provider2.toString();
}
@Override
public void syncInfoSetReset(SyncInfoSet set, IProgressMonitor monitor) {
if(provider.isDisposed()) {
set.removeSyncSetChangedListener(this);
} else {
reset();
}
}
@Override
public void syncInfoChanged(final ISyncInfoSetChangeEvent event, IProgressMonitor monitor) {
if (! (event instanceof ISyncInfoTreeChangeEvent)) {
reset();
} else {
queueEvent(new SyncInfoSetChangeEvent(event), false);
}
}
/*
* Handle the sync info set change event in the UI thread.
*/
private void handleChanges(final ISyncInfoSetChangeEvent event, final IProgressMonitor monitor) {
runViewUpdate(() -> {
provider.handleChanges((ISyncInfoTreeChangeEvent)event, monitor);
firePendingLabelUpdates();
}, true /* preserve expansion */);
}
@Override
public void syncInfoSetErrors(SyncInfoSet set, ITeamStatus[] errors, IProgressMonitor monitor) {
// When errors occur we currently don't process them. It may be possible to decorate
// elements in the model with errors, but currently we prefer to let ignore and except
// another listener to display them.
}
public ISynchronizeModelProvider getProvider() {
return provider;
}
public void connect(IProgressMonitor monitor) {
getProvider().getSyncInfoSet().connect(this, monitor);
}
public void runViewUpdate(final Runnable runnable, final boolean preserveExpansion) {
if (Utils.canUpdateViewer(getViewer()) || isPerformingBackgroundUpdate()) {
internalRunViewUpdate(runnable, preserveExpansion);
} else {
if (Thread.currentThread() != getEventHandlerJob().getThread()) {
// Run view update should only be called from the UI thread or
// the update handler thread.
// We will log the problem for now and make it an assert later
TeamUIPlugin.log(IStatus.WARNING, "View update invoked from invalid thread", new TeamException("View update invoked from invalid thread")); //$NON-NLS-1$ //$NON-NLS-2$
}
final Control ctrl = getViewer().getControl();
if (ctrl != null && !ctrl.isDisposed()) {
ctrl.getDisplay().syncExec(() -> {
if (!ctrl.isDisposed()) {
BusyIndicator.showWhile(ctrl.getDisplay(), () -> internalRunViewUpdate(runnable, preserveExpansion));
}
});
}
}
}
/*
* Return whether the event handler is performing a background view update.
* In other words, a client has invoked <code>performUpdate</code>.
*/
public boolean isPerformingBackgroundUpdate() {
return Thread.currentThread() == getEventHandlerJob().getThread() && performingBackgroundUpdate;
}
/*
* Method that can be called from the UI thread to update the view model.
*/
private void internalRunViewUpdate(final Runnable runnable, boolean preserveExpansion) {
StructuredViewer viewer = getViewer();
IResource[] expanded = null;
IResource[] selected = null;
try {
if (Utils.canUpdateViewer(viewer)) {
viewer.getControl().setRedraw(false);
if (preserveExpansion) {
expanded = provider.getExpandedResources();
selected = provider.getSelectedResources();
}
if (viewer instanceof AbstractTreeViewer && additionsMap == null)
additionsMap = new HashMap<>();
}
runnable.run();
} finally {
if (Utils.canUpdateViewer(viewer)) {
try {
if (additionsMap != null && !additionsMap.isEmpty() && Utils.canUpdateViewer(viewer)) {
for (ISynchronizeModelElement parent : additionsMap.keySet()) {
if (Policy.DEBUG_SYNC_MODELS) {
System.out.println("Adding child view items of " + parent.getName()); //$NON-NLS-1$
}
Set<ISynchronizeModelElement> toAdd = additionsMap.get(parent);
((AbstractTreeViewer)viewer).add(parent, toAdd.toArray(new Object[toAdd.size()]));
}
additionsMap = null;
}
if (expanded != null) {
provider.expandResources(expanded);
}
if (selected != null) {
provider.selectResources(selected);
}
} finally {
viewer.getControl().setRedraw(true);
}
}
}
ISynchronizeModelElement root = provider.getModelRoot();
if(root instanceof SynchronizeModelElement)
((SynchronizeModelElement)root).fireChanges();
}
/**
* Execute a runnable which performs an update of the model being displayed
* by the handler's provider. The runnable should be executed in a thread-safe manner
* which esults in the view being updated.
* @param runnable the runnable which updates the model.
* @param preserveExpansion whether the expansion of the view should be preserver
* @param updateInUIThread if <code>true</code>, the model will be updated in the
* UI thread. Otherwise, the model will be updated in the handler thread and the view
* updated in the UI thread at the end.
*/
public void performUpdate(final IWorkspaceRunnable runnable, boolean preserveExpansion, boolean updateInUIThread) {
if (updateInUIThread) {
queueEvent(new BackgroundEventHandler.RunnableEvent(getUIUpdateRunnable(runnable, preserveExpansion), true), true);
} else {
queueEvent(new BackgroundEventHandler.RunnableEvent(getBackgroundUpdateRunnable(runnable, preserveExpansion), true), true);
}
}
/**
* Wrap the runnable in an outer runnable that preserves expansion.
*/
private IWorkspaceRunnable getUIUpdateRunnable(final IWorkspaceRunnable runnable, final boolean preserveExpansion) {
return monitor -> {
final CoreException[] exception = new CoreException[] { null };
runViewUpdate(() -> {
try {
runnable.run(monitor);
} catch (CoreException e) {
exception[0] = e;
}
}, true /* preserve expansion */);
if (exception[0] != null)
throw exception[0];
};
}
/*
* Wrap the runnable in an outer runnable that preserves expansion if requested
* and refreshes the view when the update is completed.
*/
private IWorkspaceRunnable getBackgroundUpdateRunnable(final IWorkspaceRunnable runnable, final boolean preserveExpansion) {
return new IWorkspaceRunnable() {
IResource[] expanded;
IResource[] selected;
@Override
public void run(IProgressMonitor monitor) throws CoreException {
if (preserveExpansion)
recordExpandedResources();
try {
performingBackgroundUpdate = true;
runnable.run(monitor);
} finally {
performingBackgroundUpdate = false;
}
updateView();
}
private void recordExpandedResources() {
final StructuredViewer viewer = getViewer();
if (viewer != null && !viewer.getControl().isDisposed() && viewer instanceof AbstractTreeViewer) {
viewer.getControl().getDisplay().syncExec(() -> {
if (viewer != null && !viewer.getControl().isDisposed()) {
expanded = provider.getExpandedResources();
selected = provider.getSelectedResources();
}
});
}
}
private void updateView() {
// Refresh the view and then set the expansion
runViewUpdate(() -> {
provider.getViewer().refresh();
if (expanded != null)
provider.expandResources(expanded);
if (selected != null)
provider.selectResources(selected);
}, false /* do not preserve expansion (since it is done above) */);
}
};
}
/*
* Execute the RunnableEvent
*/
private void executeRunnable(Event event, IProgressMonitor monitor) {
try {
// Dispatch any queued results to clear pending output events
dispatchEvents(Policy.subMonitorFor(monitor, 1));
} catch (TeamException e) {
handleException(e);
}
try {
((RunnableEvent)event).run(Policy.subMonitorFor(monitor, 1));
} catch (CoreException e) {
handleException(e);
}
}
/**
* Add the element to the viewer.
* @param parent the parent of the element which is already added to the viewer
* @param element the element to be added to the viewer
*/
protected void doAdd(ISynchronizeModelElement parent, ISynchronizeModelElement element) {
if (additionsMap == null) {
if (Policy.DEBUG_SYNC_MODELS) {
System.out.println("Added view item " + element.getName()); //$NON-NLS-1$
}
AbstractTreeViewer viewer = (AbstractTreeViewer)getViewer();
viewer.add(parent, element);
} else {
// Accumulate the additions
if (Policy.DEBUG_SYNC_MODELS) {
System.out.println("Queueing view item for addition " + element.getName()); //$NON-NLS-1$
}
Set<ISynchronizeModelElement> toAdd = additionsMap.get(parent);
if (toAdd == null) {
toAdd = new HashSet<>();
additionsMap.put(parent, toAdd);
}
toAdd.add(element);
}
}
}