blob: 6f24c79e50e8dde74a08e7595f9cc2365b0ebb97 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.team.core.synchronize;
import java.util.*;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.ILock;
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.*;
import org.eclipse.team.internal.core.subscribers.SyncInfoStatistics;
import org.eclipse.team.internal.core.subscribers.SyncSetChangedEvent;
/**
* 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 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 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 (SyncInfo[]) 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 resources = new ArrayList();
for (int i = 0; i < infos.length; i++) {
SyncInfo info = infos[i];
resources.add(info.getLocal());
}
return (IResource[]) 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>
*/
public synchronized SyncInfo getSyncInfo(IResource resource) {
return (SyncInfo)resources.get(resource.getFullPath());
}
/**
* Return the number of out-of-sync resources contained in this set.
* @return the size of the set.
*/
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() & mask) == kind</code>).
* @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.
*/
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 = (SyncInfo)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 = (SyncInfo)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);
}
}
/**
* Deregisters the given listener for participant notifications. Has
* no effect if listener is not already registered.
*
* @param listener listener to deregister
*/
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 gaurenteed 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.
*
* @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>
* Disconnecting is done by calling <code>removeChangeListener</code>. Once disconnected,
* a listener can reconnect to be reinitialized.
* @param listener
* @param monitor
*/
public void connect(final ISyncInfoSetChangeListener listener, IProgressMonitor monitor) {
run(new IWorkspaceRunnable() {
public void run(IProgressMonitor monitor) {
try {
monitor.beginTask(null, 100);
addSyncSetChangedListener(listener);
listener.syncInfoSetReset(SyncInfoSet.this, Policy.subMonitorFor(monitor, 95));
} finally {
monitor.done();
}
}
}, monitor);
}
private ILock lock = Platform.getJobManager().newLock();
private Set listeners = Collections.synchronizedSet(new HashSet());
private SyncSetChangedEvent 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.
* 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>.
* @param info
*/
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 syncinfo 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 synchronized void remove(IResource resource) {
try {
beginInput();
SyncInfo info = internalRemove(resource);
getChangeEvent().removed(resource, info);
} 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
*/
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
*/
public SyncInfo[] getNodes(FastSyncInfoFilter filter) {
List 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 (SyncInfo[]) 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.
*/
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.
*/
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.
* <pre>
* try {
* set.beginInput();
* // do stuff
* } finally {
* set.endInput(progress);
* }
* </pre>
* Calls to <code>beginInput</code> and <code>endInput</code> can be nested and must be matched.
*/
public void beginInput() {
lock.acquire();
}
/**
* This method is used to release the lock on this set. The prgress monitor is needed to allow
* listeners to perform long-running operations is reponse to the set change. The lock is held
* while the listeners are notified so listeners must be cautious in order to avoid deadlock.
*/
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
*/
protected SyncSetChangedEvent createEmptyChangeEvent() {
return new SyncSetChangedEvent(this);
}
private void fireChanges(final IProgressMonitor monitor) {
// Use a synchronized block to ensure that the event we send is static
final SyncSetChangedEvent event;
synchronized(this) {
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];
Platform.run(new ISafeRunnable() {
public void handleException(Throwable exception) {
// don't log the exception....it is already being logged in Platform#run
}
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 = (ISyncInfoSetChangeListener[]) 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 sublasses to access the event.
* @return Returns the changes.
*/
protected SyncSetChangedEvent 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.
* 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 relevent information.
* The error will remain in the set until the set is reset.
* @param resource the resource associated with the error or the workspace root
* @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 (ITeamStatus[]) errors.values().toArray(new ITeamStatus[errors.size()]);
}
}