| /******************************************************************************* |
| * Copyright (c) 2000, 2017 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.variants; |
| |
| import java.util.*; |
| |
| import org.eclipse.core.resources.*; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.core.runtime.jobs.*; |
| import org.eclipse.team.core.TeamException; |
| import org.eclipse.team.internal.core.Policy; |
| import org.eclipse.team.internal.core.subscribers.BatchingLock; |
| import org.eclipse.team.internal.core.subscribers.SyncByteConverter; |
| import org.eclipse.team.internal.core.subscribers.BatchingLock.IFlushOperation; |
| import org.eclipse.team.internal.core.subscribers.BatchingLock.ThreadInfo; |
| |
| /** |
| * This class manages the synchronization between local resources and their |
| * corresponding resource variants. It provides the following: |
| * <ul> |
| * <li>Three way synchronization (set base, set remote, ignored) |
| * <li>Resource traversal (members) |
| * <li>Change events and event batching (run) |
| * <li>Thread-safety |
| * </ul> |
| * |
| * @since 3.0 |
| */ |
| public class ThreeWaySynchronizer { |
| |
| private IFlushOperation flushOperation = new IFlushOperation() { |
| /** |
| * Callback which is invoked when the batching resource lock is released |
| * or when a flush is requested (see beginBatching(IResource)). |
| * |
| * @see BatchingLock#flush(IProgressMonitor) |
| */ |
| @Override |
| public void flush(ThreadInfo info, IProgressMonitor monitor) |
| throws TeamException { |
| if (info != null && !info.isEmpty()) { |
| broadcastSyncChanges(info.getChangedResources()); |
| } |
| } |
| }; |
| |
| private static final byte[] IGNORED_BYTES = "i".getBytes(); //$NON-NLS-1$ |
| |
| private ILock lock = Job.getJobManager().newLock(); |
| private BatchingLock batchingLock = new BatchingLock(); |
| private ResourceVariantByteStore cache; |
| private Set<ISynchronizerChangeListener> listeners = new HashSet<>(); |
| |
| /** |
| * Create a three-way synchronizer that uses a persistent |
| * byte store with the given qualified name as its unique |
| * identifier. |
| * @param name the unique identifier for the persistent store |
| */ |
| public ThreeWaySynchronizer(QualifiedName name) { |
| this(new PersistantResourceVariantByteStore(name)); |
| } |
| |
| /** |
| * Create a three-way synchronizer that uses the given byte store |
| * as its underlying byte cache. |
| * @param store the byte store this synchronizer uses to cache its bytes |
| */ |
| public ThreeWaySynchronizer(ResourceVariantByteStore store) { |
| cache = store; |
| } |
| |
| /** |
| * Adds a listener to this synchronizer. Listeners will be notified |
| * when the synchronization state of a resource changes. Listeners |
| * are not notified when files are modified locally. Clients can |
| * make use of the <code>IResource</code> delta mechanism if they |
| * need to know about local modifications. |
| * Has no effect if an identical listener is already registered. |
| * <p> |
| * Team resource change listeners are informed about state changes |
| * that affect the resources supervised by this subscriber.</p> |
| * |
| * @param listener a synchronizer change listener |
| */ |
| public void addListener(ISynchronizerChangeListener listener) { |
| synchronized (listeners) { |
| listeners.add(listener); |
| } |
| } |
| |
| /** |
| * Removes a listener previously registered with this synchronizer. |
| * Has no effect if an identical listener is not registered. |
| * |
| * @param listener a synchronizer change listener |
| */ |
| public void removeListener(ISynchronizerChangeListener listener) { |
| synchronized (listeners) { |
| listeners.remove(listener); |
| } |
| } |
| |
| /** |
| * Return the base bytes that are cached for the given resource |
| * or <code>null</code> if no base is cached. The returned bytes |
| * should uniquely identify the resource variant that is the base |
| * for the given local resource. |
| * |
| * @param resource the resource |
| * @return the base bytes cached with the resource or <code>null</code> |
| * @throws TeamException |
| */ |
| public byte[] getBaseBytes(IResource resource) throws TeamException { |
| try { |
| beginOperation(); |
| byte[] syncBytes = internalGetSyncBytes(resource); |
| if (syncBytes == null) return null; |
| byte[] baseBytes = getSlot(syncBytes, 1); |
| if (baseBytes == null || baseBytes.length == 0) return null; |
| return baseBytes; |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /** |
| * Set the base bytes for the given resource. The provided bytes |
| * should encode enough information to uniquely identify |
| * (and possibly recreate) the resource variant that is the base |
| * for the given local resource. In essence, setting the base |
| * bytes is equivalent to marking the file as in-sync. As such, |
| * setting the base bytes will also set the remote bytes and mark |
| * the file as clean (i.e. having no outgoing changes). |
| * |
| * @param resource the resource |
| * @param baseBytes the base bytes that identify the base resource variant |
| * @throws TeamException |
| */ |
| public void setBaseBytes(IResource resource, byte[] baseBytes) throws TeamException { |
| Assert.isNotNull(baseBytes); |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(resource, null); |
| try { |
| beginOperation(); |
| String base = new String(baseBytes); |
| String[] slots = new String[] { |
| new Long(resource.getModificationStamp()).toString(), |
| base, |
| base |
| }; |
| byte[] syncBytes = toBytes(slots); |
| internalSetSyncBytes(resource, syncBytes); |
| batchingLock.resourceChanged(resource); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * Return whether the local resource has been modified since the last time |
| * the base bytes were set. This method will return <code>false</code> |
| * for ignored resources and <code>true</code> for non-existant resources |
| * that have base bytes cached. |
| * @param resource the resource |
| * @return <code>true</code> if the resource has been modified since the |
| * last time the base bytes were set. |
| * @throws TeamException |
| */ |
| public boolean isLocallyModified(IResource resource) throws TeamException { |
| return ((internalGetSyncBytes(resource) == null && ! isIgnored(resource)) || |
| (getLocalTimestamp(resource) != resource.getModificationStamp()) || |
| (getBaseBytes(resource) != null && !resource.exists())); |
| } |
| |
| /** |
| * Return the remote bytes that are cached for the given resource |
| * or <code>null</code> if no remote is cached. The returned bytes |
| * should uniquely identify the resource variant that is the remote |
| * for the given local resource. |
| * |
| * @param resource the resource |
| * @return the remote bytes cached with the resource or <code>null</code> |
| * @throws TeamException |
| */ |
| public byte[] getRemoteBytes(IResource resource) throws TeamException { |
| try { |
| beginOperation(); |
| byte[] syncBytes = internalGetSyncBytes(resource); |
| if (syncBytes == null) return null; |
| byte[] remoteBytes = getSlot(syncBytes, 2); |
| if (remoteBytes == null || remoteBytes.length == 0) return null; |
| return remoteBytes; |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /** |
| * Set the remote bytes for the given resource. The provided bytes |
| * should encode enough information to uniquely identify |
| * (and possibly recreate) the resource variant that is the remote |
| * for the given local resource. If the remote for a resource |
| * no longer exists, <code>removeRemoteBytes(IResource)</code> |
| * should be called. |
| * |
| * @param resource the resource |
| * @param remoteBytes the base bytes that identify the remote resource variant |
| * @return <code>true</code> if the remote bytes changed as a result of the set |
| * @throws TeamException |
| */ |
| public boolean setRemoteBytes(IResource resource, byte[] remoteBytes) throws TeamException { |
| Assert.isNotNull(remoteBytes); |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(resource, null); |
| try { |
| beginOperation(); |
| byte[] syncBytes = internalGetSyncBytes(resource); |
| if (syncBytes == null) { |
| String[] slots = new String[] { |
| "", //$NON-NLS-1$ |
| "", //$NON-NLS-1$ |
| new String(remoteBytes) |
| }; |
| syncBytes = toBytes(slots); |
| } else { |
| byte[] currentRemote = getSlot(syncBytes, 2); |
| if (equals(remoteBytes, currentRemote)) return false; |
| syncBytes = setSlot(syncBytes, 2, remoteBytes); |
| } |
| internalSetSyncBytes(resource, syncBytes); |
| batchingLock.resourceChanged(resource); |
| return true; |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * Remove the remote bytes associated with the resource. This is typically |
| * done when the corresponding remote resource variant no longer exists. |
| * @param resource the resource |
| * @return <code>true</code> if the remote bytes changed as a result of the removal |
| * @throws TeamException |
| */ |
| public boolean removeRemoteBytes(IResource resource) throws TeamException { |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(resource, null); |
| try { |
| beginOperation(); |
| byte[] syncBytes = internalGetSyncBytes(resource); |
| if (syncBytes != null) { |
| String currentRemote = new String(getSlot(syncBytes, 2)); |
| if (currentRemote.length() == 0) return false; |
| syncBytes = setSlot(syncBytes, 2, new byte[0]); |
| internalSetSyncBytes(resource, syncBytes); |
| batchingLock.resourceChanged(resource); |
| return true; |
| } |
| return false; |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * Return whether the given resource has sync bytes in the synchronizer. |
| * @param resource the local resource |
| * @return whether there are sync bytes cached for the local resources. |
| * @throws TeamException |
| */ |
| public boolean hasSyncBytes(IResource resource) throws TeamException { |
| return internalGetSyncBytes(resource) != null; |
| } |
| |
| /** |
| * Returns whether the resource has been marked as ignored |
| * using <code>setIgnored(IResource)</code>. |
| * @param resource the resource |
| * @return <code>true</code> if the resource is ignored. |
| * @throws TeamException |
| */ |
| public boolean isIgnored(IResource resource) throws TeamException { |
| byte[] bytes = cache.getBytes(resource); |
| return (bytes != null && equals(bytes, IGNORED_BYTES)); |
| } |
| |
| /** |
| * Mark the resource as being ignored. Ignored resources |
| * are not returned by the <code>members</code> method, |
| * are never dirty (see <code>isLocallyModified</code>) and |
| * do not have base or remote bytes cached for them. |
| * @param resource the resource to be ignored |
| * @throws TeamException |
| */ |
| public void setIgnored(IResource resource) throws TeamException { |
| internalSetSyncBytes(resource, IGNORED_BYTES); |
| } |
| |
| /** |
| * Return the members of the local resource that either have sync bytes |
| * or exist locally and are not ignored. |
| * @param resource the local resource |
| * @return the children of the local resource that have cached sync bytes |
| * or are not ignored |
| * @throws TeamException |
| */ |
| public IResource[] members(IResource resource) throws TeamException { |
| if (resource.getType() == IResource.FILE) { |
| return new IResource[0]; |
| } |
| try { |
| Set<IResource> potentialChildren = new HashSet<>(); |
| IContainer container = (IContainer)resource; |
| if (container.exists()) { |
| potentialChildren.addAll(Arrays.asList(container.members())); |
| } |
| potentialChildren.addAll(Arrays.asList(cache.members(resource))); |
| List<IResource> result = new ArrayList<>(); |
| for (IResource child : potentialChildren) { |
| if (child.exists() || hasSyncBytes(child)) { |
| result.add(child); |
| } |
| } |
| return result.toArray(new IResource[result.size()]); |
| } catch (CoreException e) { |
| throw TeamException.asTeamException(e); |
| } |
| } |
| |
| /** |
| * Flush any cached bytes for the given resource to the depth specified. |
| * @param resource the resource |
| * @param depth the depth of the flush (one of <code>IResource.DEPTH_ZERO</code>, |
| * <code>IResource.DEPTH_ONE</code>, or <code>IResource.DEPTH_INFINITE</code>) |
| * @throws TeamException |
| */ |
| public void flush(IResource resource, int depth) throws TeamException { |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(resource, null); |
| try { |
| beginOperation(); |
| if (cache.flushBytes(resource, depth)) { |
| batchingLock.resourceChanged(resource); |
| } |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * Perform multiple sync state modifications and fire only a single change notification |
| * at the end. |
| * @param resourceRule the scheduling rule that encompasses all modifications |
| * @param runnable the runnable that performs the sync state modifications |
| * @param monitor a progress monitor |
| * @throws TeamException |
| */ |
| public void run(IResource resourceRule, IWorkspaceRunnable runnable, IProgressMonitor monitor) throws TeamException { |
| monitor = Policy.monitorFor(monitor); |
| monitor.beginTask(null, 100); |
| ISchedulingRule rule = beginBatching(resourceRule, Policy.subMonitorFor(monitor, 10)); |
| try { |
| cache.run(resourceRule, runnable, Policy.subMonitorFor(monitor, 80)); |
| } catch (CoreException e) { |
| throw TeamException.asTeamException(e); |
| } finally { |
| if (rule != null) endBatching(rule, Policy.subMonitorFor(monitor, 10)); |
| monitor.done(); |
| } |
| } |
| |
| private void broadcastSyncChanges(final IResource[] resources) { |
| ISynchronizerChangeListener[] allListeners; |
| // Copy the listener list so we're not calling client code while synchronized |
| synchronized(listeners) { |
| allListeners = listeners.toArray(new ISynchronizerChangeListener[listeners.size()]); |
| } |
| // Notify the listeners safely so all will receive notification |
| for (int i = 0; i < allListeners.length; i++) { |
| final ISynchronizerChangeListener 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 { |
| listener.syncStateChanged(resources); |
| |
| } |
| }); |
| } |
| } |
| |
| /* |
| * Return the cached sync bytes for the given resource. |
| * The value <code>null</code> is returned if there is no |
| * cached bytes or if the resource is ignored. |
| */ |
| private byte[] internalGetSyncBytes(IResource resource) throws TeamException { |
| byte[] bytes = cache.getBytes(resource); |
| if (bytes != null && equals(bytes, IGNORED_BYTES)) return null; |
| return bytes; |
| } |
| |
| /* |
| * Set the cached sync bytes |
| */ |
| private boolean internalSetSyncBytes(IResource resource, byte[] syncBytes) throws TeamException { |
| return cache.setBytes(resource, syncBytes); |
| } |
| |
| private byte[] getSlot(byte[] syncBytes, int i) { |
| return SyncByteConverter.getSlot(syncBytes, i, false); |
| } |
| |
| private byte[] setSlot(byte[] syncBytes, int i, byte[] insertBytes) throws TeamException { |
| return SyncByteConverter.setSlot(syncBytes, i, insertBytes); |
| } |
| |
| private byte[] toBytes(String[] slots) { |
| return SyncByteConverter.toBytes(slots); |
| } |
| |
| private long getLocalTimestamp(IResource resource) throws TeamException { |
| try { |
| beginOperation(); |
| byte[] syncBytes = internalGetSyncBytes(resource); |
| if (syncBytes == null) return -1; |
| byte[] bytes = getSlot(syncBytes, 0); |
| if (bytes == null || bytes.length == 0) return -1; |
| return Long.parseLong(new String(bytes)); |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| private boolean equals(byte[] syncBytes, byte[] oldBytes) { |
| if (syncBytes.length != oldBytes.length) return false; |
| for (int i = 0; i < oldBytes.length; i++) { |
| if (oldBytes[i] != syncBytes[i]) return false; |
| } |
| return true; |
| } |
| |
| /* |
| * Begin an access to the internal data structures of the synchronizer |
| */ |
| private void beginOperation() { |
| // Do not try to acquire the lock if the resources tree is locked |
| // The reason for this is that during the resource delta phase (i.e. when the tree is locked) |
| // the workspace lock is held. If we obtain our lock, there is |
| // a chance of deadlock. It is OK if we don't as we are still protected |
| // by scheduling rules and the workspace lock. |
| if (ResourcesPlugin.getWorkspace().isTreeLocked()) return; |
| lock.acquire(); |
| } |
| |
| /* |
| * End an access to the internal data structures of the synchronizer |
| */ |
| private void endOperation() { |
| // See beginOperation() for a description of why the lock is not obtained when the tree is locked |
| if (ResourcesPlugin.getWorkspace().isTreeLocked()) return; |
| lock.release(); |
| } |
| |
| /* |
| * Begins a batch of operations in order to batch event changes. |
| * The provided scheduling rule indicates the resources |
| * that the resources affected by the operation while the returned scheduling rule |
| * is the rule obtained by the lock. It may differ from the provided rule. |
| */ |
| private ISchedulingRule beginBatching(ISchedulingRule resourceRule, IProgressMonitor monitor) { |
| return batchingLock.acquire(resourceRule, flushOperation /* IFlushOperation */, monitor); |
| } |
| |
| /* |
| * Ends a batch of operations. The provided rule must be the one that was returned |
| * by the corresponding call to beginBatching. |
| */ |
| private void endBatching(ISchedulingRule rule, IProgressMonitor monitor) throws TeamException { |
| batchingLock.release(rule, monitor); |
| } |
| } |