blob: beb723704974a97619460f7f96069420bc325c80 [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.core.synchronize;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.jobs.ILock;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.team.core.ITeamStatus;
import org.eclipse.team.core.TeamStatus;
import org.eclipse.team.core.synchronize.FastSyncInfoFilter.SyncInfoDirectionFilter;
import org.eclipse.team.internal.core.Policy;
import org.eclipse.team.internal.core.TeamPlugin;
import org.eclipse.team.internal.core.subscribers.SyncInfoStatistics;
/**
* A dynamic collection of {@link SyncInfo} objects that provides
* change notification to registered listeners. Batching of change notifications
* can be accomplished using the <code>beginInput/endInput</code> methods.
*
* @see SyncInfoTree
* @see SyncInfo
* @see ISyncInfoSetChangeListener
* @since 3.0
*/
public class SyncInfoSet {
// fields used to hold resources of interest
// {IPath -> SyncInfo}
private Map<IPath, SyncInfo> resources = Collections.synchronizedMap(new HashMap<>());
// keep track of number of sync kinds in the set
private SyncInfoStatistics statistics = new SyncInfoStatistics();
// keep track of errors that occurred while trying to populate the set
private Map<IResource, ITeamStatus> errors = new HashMap<>();
private boolean lockedForModification;
/**
* Create an empty set.
*/
public SyncInfoSet() {
}
/**
* Create a <code>SyncInfoSet</code> containing the given <code>SyncInfo</code>
* instances.
*
* @param infos the <code>SyncInfo</code> instances to be contained by this set
*/
public SyncInfoSet(SyncInfo[] infos) {
this();
// use the internal add since we can't have listeners at this point anyway
for (int i = 0; i < infos.length; i++) {
internalAdd(infos[i]);
}
}
/**
* Return an array of <code>SyncInfo</code> for all out-of-sync resources that are contained by the set.
*
* @return an array of <code>SyncInfo</code>
*/
public synchronized SyncInfo[] getSyncInfos() {
return resources.values().toArray(new SyncInfo[resources.size()]);
}
/**
* Return all out-of-sync resources contained in this set. The default implementation
* uses <code>getSyncInfos()</code> to determine the resources contained in the set.
* Subclasses may override to optimize.
*
* @return all out-of-sync resources contained in the set
*/
public IResource[] getResources() {
SyncInfo[] infos = getSyncInfos();
List<IResource> resources = new ArrayList<>();
for (int i = 0; i < infos.length; i++) {
SyncInfo info = infos[i];
resources.add(info.getLocal());
}
return resources.toArray(new IResource[resources.size()]);
}
/**
* Return the <code>SyncInfo</code> for the given resource or <code>null</code>
* if the resource is not contained in the set.
*
* @param resource the resource
* @return the <code>SyncInfo</code> for the resource or <code>null</code> if
* the resource is in-sync or doesn't have synchronization information in this set.
*/
public synchronized SyncInfo getSyncInfo(IResource resource) {
return resources.get(resource.getFullPath());
}
/**
* Return the number of out-of-sync resources contained in this set.
*
* @return the size of the set.
* @see #countFor(int, int)
*/
public synchronized int size() {
return resources.size();
}
/**
* Return the number of out-of-sync resources in the given set whose sync kind
* matches the given kind and mask (e.g.
* <code>(SyncInfo#getKind() &amp; mask) == kind</code>).
* <p>
* For example, this will return the number of outgoing changes in the set:
* </p>
*
* <pre>
* long outgoing = countFor(SyncInfo.OUTGOING, SyncInfo.DIRECTION_MASK);
* </pre>
*
* @param kind the sync kind
* @param mask the sync kind mask
* @return the number of matching resources in the set.
*/
public long countFor(int kind, int mask) {
return statistics.countFor(kind, mask);
}
/**
* Returns <code>true</code> if there are any conflicting nodes in the set, and
* <code>false</code> otherwise.
*
* @return <code>true</code> if there are any conflicting nodes in the set, and
* <code>false</code> otherwise.
*/
public boolean hasConflicts() {
return countFor(SyncInfo.CONFLICTING, SyncInfo.DIRECTION_MASK) > 0;
}
/**
* Return whether the set is empty.
*
* @return <code>true</code> if the set is empty
*/
public synchronized boolean isEmpty() {
return resources.isEmpty();
}
/**
* Add the <code>SyncInfo</code> to the set, replacing any previously existing one.
*
* @param info the new <code>SyncInfo</code>
*/
protected synchronized void internalAdd(SyncInfo info) {
Assert.isTrue(!lockedForModification);
IResource local = info.getLocal();
IPath path = local.getFullPath();
SyncInfo oldSyncInfo = resources.put(path, info);
if(oldSyncInfo == null) {
statistics.add(info);
} else {
statistics.remove(oldSyncInfo);
statistics.add(info);
}
}
/**
* Remove the resource from the set, updating all internal data structures.
*
* @param resource the resource to be removed
* @return the <code>SyncInfo</code> that was just removed
*/
protected synchronized SyncInfo internalRemove(IResource resource) {
Assert.isTrue(!lockedForModification);
IPath path = resource.getFullPath();
SyncInfo info = resources.remove(path);
if (info != null) {
statistics.remove(info);
}
return info;
}
/**
* Registers the given listener for sync info set notifications. Has
* no effect if an identical listener is already registered.
*
* @param listener listener to register
*/
public void addSyncSetChangedListener(ISyncInfoSetChangeListener listener) {
synchronized(listeners) {
listeners.add(listener);
}
}
/**
* Removes the given listener from participant notifications. Has
* no effect if listener is not already registered.
*
* @param listener listener to remove
*/
public void removeSyncSetChangedListener(ISyncInfoSetChangeListener listener) {
synchronized(listeners) {
listeners.remove(listener);
}
}
/**
* Reset the sync set so it is empty. Listeners are notified of the change.
*/
public void clear() {
try {
beginInput();
errors.clear();
resources.clear();
statistics.clear();
getChangeEvent().reset();
} finally {
endInput(null);
}
}
/*
* Run the given runnable. This operation
* will block other threads from modifying the
* set and postpone any change notifications until after the runnable
* has been executed. Mutable subclasses must override.
* <p>
* The given runnable may be run in the same thread as the caller or
* more be run asynchronously in another thread at the discretion of the
* subclass implementation. However, it is guaranteed that two invocations
* of <code>run</code> performed in the same thread will be executed in the
* same order even if run in different threads.
* </p>
* @param runnable a runnable
* @param progress a progress monitor or <code>null</code>
*/
private void run(IWorkspaceRunnable runnable, IProgressMonitor monitor) {
monitor = Policy.monitorFor(monitor);
monitor.beginTask(null, 100);
try {
beginInput();
runnable.run(Policy.subMonitorFor(monitor, 80));
} catch (CoreException e) {
addError(new TeamStatus(IStatus.ERROR, TeamPlugin.ID, ITeamStatus.SYNC_INFO_SET_ERROR, e.getMessage(), e, null));
} finally {
endInput(Policy.subMonitorFor(monitor, 20));
}
}
/**
* Connect the listener to the sync set in such a fashion that the listener will
* be connected the the sync set using <code>addChangeListener</code>
* and issued a reset event. This is done to provide a means of connecting to the
* sync set and initializing a model based on the sync set without worrying about
* missing events.
* <p>
* The reset event may be done in the context of this method invocation or may be
* done in another thread at the discretion of the <code>SyncInfoSet</code>
* implementation.
* </p><p>
* Disconnecting is done by calling <code>removeChangeListener</code>. Once disconnected,
* a listener can reconnect to be re-initialized.
* </p>
* @param listener the listener that should be connected to this set
* @param monitor a progress monitor
*/
public void connect(final ISyncInfoSetChangeListener listener, IProgressMonitor monitor) {
run(monitor1 -> {
try {
monitor1.beginTask(null, 100);
addSyncSetChangedListener(listener);
listener.syncInfoSetReset(SyncInfoSet.this, Policy.subMonitorFor(monitor1, 95));
} finally {
monitor1.done();
}
}, monitor);
}
private ILock lock = Job.getJobManager().newLock();
private Set<ISyncInfoSetChangeListener> listeners = Collections.synchronizedSet(new HashSet<>());
private SyncInfoSetChangeEvent changes = createEmptyChangeEvent();
/**
* Add the given <code>SyncInfo</code> to the set. A change event will
* be generated unless the call to this method is nested in between calls
* to <code>beginInput()</code> and <code>endInput(IProgressMonitor)</code>
* in which case the event for this addition and any other sync set
* change will be fired in a batched event when <code>endInput</code>
* is invoked.
* <p>
* Invoking this method outside of the above mentioned block will result
* in the <code>endInput(IProgressMonitor)</code> being invoked with a null
* progress monitor. If responsiveness is required, the client should always
* nest sync set modifications within <code>beginInput/endInput</code>.
* </p>
* @param info the sync info to be added to this set.
*/
public void add(SyncInfo info) {
try {
beginInput();
boolean alreadyExists = getSyncInfo(info.getLocal()) != null;
internalAdd(info);
if (alreadyExists) {
getChangeEvent().changed(info);
} else {
getChangeEvent().added(info);
}
} finally {
endInput(null);
}
}
/**
* Add all the sync info from the given set to this set.
*
* @param set the set whose sync info should be added to this set
*/
public void addAll(SyncInfoSet set) {
try {
beginInput();
SyncInfo[] infos = set.getSyncInfos();
for (int i = 0; i < infos.length; i++) {
add(infos[i]);
}
} finally {
endInput(null);
}
}
/**
* Remove the given local resource from the set.
*
* @param resource the local resource to remove
*/
public void remove(IResource resource) {
try {
beginInput();
internalRemove(resource);
getChangeEvent().removed(resource);
} finally {
endInput(null);
}
}
/**
* Remove all the given resources from the set.
*
* @param resources the resources to be removed
*/
public void removeAll(IResource[] resources) {
try {
beginInput();
for (int i = 0; i < resources.length; i++) {
remove(resources[i]);
}
} finally {
endInput(null);
}
}
/**
* Removes all conflicting nodes from this set.
*/
public void removeConflictingNodes() {
rejectNodes(new SyncInfoDirectionFilter(SyncInfo.CONFLICTING));
}
/**
* Removes all outgoing nodes from this set.
*/
public void removeOutgoingNodes() {
rejectNodes(new SyncInfoDirectionFilter(SyncInfo.OUTGOING));
}
/**
* Removes all incoming nodes from this set.
*/
public void removeIncomingNodes() {
rejectNodes(new SyncInfoDirectionFilter(SyncInfo.INCOMING));
}
/**
* Indicate whether the set has nodes matching the given filter.
*
* @param filter a sync info filter
* @return whether the set has nodes that match the filter
*/
public boolean hasNodes(FastSyncInfoFilter filter) {
SyncInfo[] infos = getSyncInfos();
for (int i = 0; i < infos.length; i++) {
SyncInfo info = infos[i];
if (info != null && filter.select(info)) {
return true;
}
}
return false;
}
/**
* Removes all nodes from this set that do not match the given filter
* leaving only those that do match the filter.
*
* @param filter a sync info filter
*/
public void selectNodes(FastSyncInfoFilter filter) {
try {
beginInput();
SyncInfo[] infos = getSyncInfos();
for (int i = 0; i < infos.length; i++) {
SyncInfo info = infos[i];
if (info == null || !filter.select(info)) {
remove(info.getLocal());
}
}
} finally {
endInput(null);
}
}
/**
* Removes all nodes from this set that match the given filter
* leaving those that do not match the filter.
*
* @param filter a sync info filter
*/
public void rejectNodes(FastSyncInfoFilter filter) {
try {
beginInput();
SyncInfo[] infos = getSyncInfos();
for (int i = 0; i < infos.length; i++) {
SyncInfo info = infos[i];
if (info != null && filter.select(info)) {
remove(info.getLocal());
}
}
} finally {
endInput(null);
}
}
/**
* Return all nodes in this set that match the given filter.
*
* @param filter a sync info filter
* @return the nodes that match the filter
*/
public SyncInfo[] getNodes(FastSyncInfoFilter filter) {
List<SyncInfo> result = new ArrayList<>();
SyncInfo[] infos = getSyncInfos();
for (int i = 0; i < infos.length; i++) {
SyncInfo info = infos[i];
if (info != null && filter.select(info)) {
result.add(info);
}
}
return result.toArray(new SyncInfo[result.size()]);
}
/**
* Returns <code>true</code> if this sync set has incoming changes.
* Note that conflicts are not considered to be incoming changes.
*
* @return <code>true</code> if this sync set has incoming changes.
*/
public boolean hasIncomingChanges() {
return countFor(SyncInfo.INCOMING, SyncInfo.DIRECTION_MASK) > 0;
}
/**
* Returns <code>true</code> if this sync set has outgoing changes.
* Note that conflicts are not considered to be outgoing changes.
*
* @return <code>true</code> if this sync set has outgoing changes.
*/
public boolean hasOutgoingChanges() {
return countFor(SyncInfo.OUTGOING, SyncInfo.DIRECTION_MASK) > 0;
}
/**
* This method is used to obtain a lock on the set which ensures thread safety
* and batches change notification. If the set is locked by another thread, the
* calling thread will block until the lock becomes available. This method uses
* an <code>org.eclipse.core.runtime.jobs.ILock</code>.
* <p>
* It is important that the lock is released after it is obtained. Calls to
* <code>endInput</code> should be done in a finally block as illustrated in the
* following code snippet.
* </p>
*
* <pre>
* try {
* set.beginInput();
* // do stuff
* } finally {
* set.endInput(progress);
* }
* </pre>
* <p>
* Calls to <code>beginInput</code> and <code>endInput</code> can be nested and
* must be matched.
* </p>
*/
public void beginInput() {
lock.acquire();
}
/**
* This method is used to release the lock on this set. The progress monitor is needed to allow
* listeners to perform long-running operations is response to the set change. The lock is held
* while the listeners are notified so listeners must be cautious in order to avoid deadlock.
* @param monitor a progress monitor
*/
public void endInput(IProgressMonitor monitor) {
try {
if (lock.getDepth() == 1) {
// Remain locked while firing the events so the handlers
// can expect the set to remain constant while they process the events
fireChanges(Policy.monitorFor(monitor));
}
} finally {
lock.release();
}
}
/**
* Reset the changes accumulated so far by this set. This method is not
* intended to be invoked or implemented by clients.
*/
protected void resetChanges() {
changes = createEmptyChangeEvent();
}
/**
* Create an empty change event. Subclass may override to provided specialized event types
*
* @return an empty change event
* @since 3.5
*/
protected SyncInfoSetChangeEvent createEmptyChangeEvent() {
return new SyncInfoSetChangeEvent(this);
}
private void fireChanges(final IProgressMonitor monitor) {
// Only one thread at the time can enter the method, so the event we
// send is static
final SyncInfoSetChangeEvent event = getChangeEvent();
resetChanges();
// Ensure that the list of listeners is not changed while events are fired.
// Copy the listeners so that addition/removal is not blocked by event listeners
if(event.isEmpty() && ! event.isReset()) return;
ISyncInfoSetChangeListener[] allListeners = getListeners();
// Fire the events using an ISafeRunnable
final ITeamStatus[] newErrors = event.getErrors();
monitor.beginTask(null, 100 + (newErrors.length > 0 ? 50 : 0) * allListeners.length);
for (int i = 0; i < allListeners.length; i++) {
final ISyncInfoSetChangeListener listener = allListeners[i];
SafeRunner.run(new ISafeRunnable() {
@Override
public void handleException(Throwable exception) {
// don't log the exception....it is already being logged in Platform#run
}
@Override
public void run() throws Exception {
try {
lockedForModification = true;
if (event.isReset()) {
listener.syncInfoSetReset(SyncInfoSet.this, Policy.subMonitorFor(monitor, 100));
} else {
listener.syncInfoChanged(event, Policy.subMonitorFor(monitor, 100));
}
if (newErrors.length > 0) {
listener.syncInfoSetErrors(SyncInfoSet.this, newErrors, Policy.subMonitorFor(monitor, 50));
}
} finally {
lockedForModification = false;
}
}
});
}
monitor.done();
}
/**
* Return a copy of all the listeners registered with this set
* @return the listeners
*/
protected ISyncInfoSetChangeListener[] getListeners() {
ISyncInfoSetChangeListener[] allListeners;
synchronized(listeners) {
allListeners = listeners.toArray(new ISyncInfoSetChangeListener[listeners.size()]);
}
return allListeners;
}
/**
* Return the change event that is accumulating the changes to the set. This
* can be called by subclasses to access the event.
*
* @return Returns the changes.
* @nooverride This method is not intended to be re-implemented or extended
* by clients.
* @since 3.5
*/
protected SyncInfoSetChangeEvent getChangeEvent() {
return changes;
}
/**
* Add the error to the set. Errors should be added to the set when the client
* populating the set cannot determine the <code>SyncInfo</code> for one
* or more resources due to an exception or some other problem. Listeners
* will be notified that an error occurred and can react accordingly.
* <p>
* Only one error can be associated with a resource (which is obtained from
* the <code>ITeamStatus</code>). It is up to the
* client populating the set to ensure that the error associated with a
* resource contains all relevant information.
* The error will remain in the set until the set is reset.
* </p>
* @param status the status that describes the error that occurred.
*/
public void addError(ITeamStatus status) {
try {
beginInput();
errors.put(status.getResource(), status);
getChangeEvent().errorOccurred(status);
} finally {
endInput(null);
}
}
/**
* Return an array of the errors the occurred while populating this set.
* The errors will remain with the set until it is reset.
*
* @return the errors
*/
public ITeamStatus[] getErrors() {
return errors.values().toArray(new ITeamStatus[errors.size()]);
}
/**
* Return an iterator over all <code>SyncInfo</code>
* contained in this set.
* @return an iterator over all <code>SyncInfo</code>
* contained in this set.
* @since 3.1
*/
public Iterator iterator() {
return resources.values().iterator();
}
}