/******************************************************************************* * Copyright (c) 2000, 2009 IBM Corporation 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: * 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.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 beginInput/endInput 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 SyncInfoSet containing the given SyncInfo * instances. * * @param infos the SyncInfo 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 SyncInfo for all out-of-sync resources that are contained by the set. * * @return an array of SyncInfo */ 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 getSyncInfos() 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 SyncInfo for the given resource or null * if the resource is not contained in the set. * * @param resource the resource * @return the SyncInfo for the resource or null if * the resource is in-sync or doesn't have synchronization information in this set. */ 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. * @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. (SyncInfo#getKind() & mask) == kind). *

* For example, this will return the number of outgoing changes in the set: *

	 *  long outgoing =  countFor(SyncInfo.OUTGOING, SyncInfo.DIRECTION_MASK);
	 * 
*

* @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 true if there are any conflicting nodes in the set, and * false otherwise. * * @return true if there are any conflicting nodes in the set, and * false otherwise. */ public boolean hasConflicts() { return countFor(SyncInfo.CONFLICTING, SyncInfo.DIRECTION_MASK) > 0; } /** * Return whether the set is empty. * * @return true if the set is empty */ public synchronized boolean isEmpty() { return resources.isEmpty(); } /** * Add the SyncInfo to the set, replacing any previously existing one. * * @param info the new SyncInfo */ 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 SyncInfo 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); } } /** * 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. *

* 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 run 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 null */ 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 addChangeListener * 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. *

* 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 SyncInfoSet * implementation. *

* Disconnecting is done by calling removeChangeListener. Once disconnected, * a listener can reconnect to be re-initialized. *

* @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(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 = Job.getJobManager().newLock(); private Set listeners = Collections.synchronizedSet(new HashSet()); private SyncInfoSetChangeEvent changes = createEmptyChangeEvent(); /** * Add the given SyncInfo to the set. A change event will * be generated unless the call to this method is nested in between calls * to beginInput() and endInput(IProgressMonitor) * in which case the event for this addition and any other sync set * change will be fired in a batched event when endInput * is invoked. *

* Invoking this method outside of the above mentioned block will result * in the endInput(IProgressMonitor) being invoked with a null * progress monitor. If responsiveness is required, the client should always * nest sync set modifications within beginInput/endInput. *

* @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 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 true if this sync set has incoming changes. * Note that conflicts are not considered to be incoming changes. * * @return true if this sync set has incoming changes. */ public boolean hasIncomingChanges() { return countFor(SyncInfo.INCOMING, SyncInfo.DIRECTION_MASK) > 0; } /** * Returns true if this sync set has outgoing changes. * Note that conflicts are not considered to be outgoing changes. * * @return true 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 org.eclipse.core.runtime.jobs.ILock. *

* It is important that the lock is released after it is obtained. Calls to endInput * should be done in a finally block as illustrated in the following code snippet. *

	 *   try {
	 *       set.beginInput();
	 *       // do stuff
	 *   } finally {
	 *      set.endInput(progress);
	 *   }
	 * 
*

* Calls to beginInput and endInput can be nested and must be matched. *

*/ 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() { 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 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 SyncInfo 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 ITeamStatus). 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. *

* @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()]); } /** * Return an iterator over all SyncInfo * contained in this set. * @return an iterator over all SyncInfo * contained in this set. * @since 3.1 */ public Iterator iterator() { return resources.values().iterator(); } }