Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core')
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/AdapterFactory.java35
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/BackgroundEventHandler.java515
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Cache.java96
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultFileModificationValidator.java123
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultMoveDeleteHook.java86
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultProjectSetCapability.java24
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ExceptionCollector.java107
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/FileContentManager.java260
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/FileModificationValidatorManager.java103
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/IRepositoryProviderListener.java22
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/InfiniteSubProgressMonitor.java94
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Messages.java131
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/MoveDeleteManager.java104
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/NullSubProgressMonitor.java53
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/PessimisticResourceRuleFactory.java59
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/PluginStringMappings.java83
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Policy.java63
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/RepositoryProviderManager.java56
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ResourceVariantCache.java248
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ResourceVariantCacheEntry.java217
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StorageMergerDescriptor.java47
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StorageMergerRegistry.java170
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StringMatcher.java410
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamHookDispatcher.java105
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamPlugin.java225
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamResourceChangeListener.java156
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/UserStringMappings.java116
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/history/LocalFileHistory.java128
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/history/LocalFileRevision.java204
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/importing/BundleImporterExtension.java104
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/AbstractResourceMappingScope.java75
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/CompoundResourceTraversal.java291
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/DiffChangeEvent.java116
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/GroupProgressMonitor.java36
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/IStreamMergerDelegate.java33
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/LineComparator.java145
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/LocalResourceVariant.java54
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ModelProviderResourceMapping.java91
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/PathTree.java331
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceMappingInputScope.java100
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceMappingScope.java143
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceVariantFileRevision.java83
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ScopeChangeEvent.java115
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ScopeManagerEventHandler.java81
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/SyncInfoToDiffConverter.java349
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/TextStorageMerger.java97
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/messages.properties110
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/CRLFtoLFInputStream.java160
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/LFtoCRLFInputStream.java151
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/PollingInputStream.java202
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/PollingOutputStream.java193
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/ProgressMonitorInputStream.java144
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/SizeConstrainedInputStream.java144
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/TimeoutInputStream.java332
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/TimeoutOutputStream.java289
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/AbstractContentComparator.java106
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/AbstractSynchronizationScope.java109
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ActiveChangeSet.java228
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ActiveChangeSetManager.java444
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/BatchingChangeSetManager.java179
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/BatchingLock.java334
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ChangeSet.java117
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ChangeSetManager.java240
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/CheckedInChangeSet.java159
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ContentComparator.java89
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ContentComparisonDiffFilter.java65
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DescendantResourceVariantByteStore.java173
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DiffChangeSet.java184
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DiffTreeStatistics.java129
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/IChangeSetChangeListener.java59
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/RootResourceSynchronizationScope.java105
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberChangeSetManager.java355
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberDiffTreeEventHandler.java338
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberEventHandler.java415
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberResourceCollector.java233
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoCollector.java207
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoEventHandler.java217
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoSet.java74
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncByteConverter.java130
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoStatistics.java108
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoTreeChangeEvent.java97
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoWorkingSetFilter.java105
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInput.java93
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInputFromSubscriber.java56
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInputFromSyncSet.java109
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ThreeWayBaseTree.java109
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/WorkingSetFilteredSyncInfoCollector.java127
-rw-r--r--bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/WorkingSetSyncSetInput.java31
88 files changed, 13533 insertions, 0 deletions
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/AdapterFactory.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/AdapterFactory.java
new file mode 100644
index 000000000..9b5ca39cb
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/AdapterFactory.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core;
+
+import org.eclipse.core.resources.mapping.ModelProvider;
+import org.eclipse.core.resources.mapping.ResourceMapping;
+import org.eclipse.core.runtime.IAdapterFactory;
+import org.eclipse.team.internal.core.mapping.ModelProviderResourceMapping;
+
+public class AdapterFactory implements IAdapterFactory {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> T getAdapter(Object adaptableObject, Class<T> adapterType) {
+ if (adaptableObject instanceof ModelProvider && adapterType == ResourceMapping.class) {
+ ModelProvider mp = (ModelProvider) adaptableObject;
+ return (T) new ModelProviderResourceMapping(mp);
+ }
+ return null;
+ }
+
+ @Override
+ public Class<?>[] getAdapterList() {
+ return new Class[] { ResourceMapping.class};
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/BackgroundEventHandler.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/BackgroundEventHandler.java
new file mode 100644
index 000000000..f21642fbd
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/BackgroundEventHandler.java
@@ -0,0 +1,515 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRunnable;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.*;
+import org.eclipse.team.core.TeamException;
+
+/**
+ * This class provides the infrastructure for processing/dispatching of events using a
+ * background job. This is useful in the following situations.
+ * <ul>
+ * <li>an operation is potentially long running but a responsive UI is desired
+ * while the operation is being performed. To do this incoming events are processed
+ * and resulting outgoing events are queued and then dispatched at an appropriate time,
+ * thus batching UI updates.</li>
+ * <li>a change is a POST_CHANGE delta requires further modifications to the workspace
+ * which cannot be performed in the delta handler because the workspace is locked.</li>
+ * <li>a data structure is not thread safe and requires serialized operations.<li>
+ * </ul>
+ * </p>
+ * <p>
+ * The event handler has the following characteristics:
+ * <ol>
+ * <li>Incoming events are placed in an incoming queue.</li>
+ * <li>Each event is processed by calling the <code>processEvent</code> method
+ * which is implemented by the subclass. The implementation may choose to process events
+ * directly or queue events on an outgoing event queue</li>
+ * <li>The <code>doDispatchEvents</code> method of the subclass is called at certain intervals
+ * to give the subclass a chance to dispatch the events in it's outgoing queue. The interval between
+ * the first 3 dispatches will be the <code>shortDispatchDelay</code> and subsequent intervals will be
+ * the <code>longDispatchDelay</code>. This is done to avoid constantly hammering the UI for long running
+ * operations.<li>
+ * <li>Errors that occur during event processing or dispatch can be accumulated by calling the <code>handle</code>
+ * method. Accumulated errors are used to form the status that is returned when the job completes.<li>
+ * </ul>
+ * </p>
+ *
+ * @since 3.0
+ */
+public abstract class BackgroundEventHandler {
+
+ /**
+ * Event type constant used to identify a runnable event
+ */
+ public static final int RUNNABLE_EVENT = 1000;
+
+ // Events that need to be processed
+ private List<Event> awaitingProcessing = new ArrayList<>();
+
+ // The job that runs when events need to be processed
+ private Job eventHandlerJob;
+
+ // Indicate if the event handler has been shutdown
+ private boolean shutdown;
+
+ // Accumulate exceptions that occur
+ private ExceptionCollector errors;
+
+ // time the last dispatch occurred
+ private long timeOfLastDispatch = 0L;
+
+ // the number of dispatches that have occurred since the job started
+ private int dispatchCount;
+
+ // time between event dispatches
+ private static final long DISPATCH_DELAY = 1500;
+
+ // time between dispatches if the dispatch threshold has been exceeded
+ private static final long LONG_DISPATCH_DELAY = 10000;
+
+ // the number of dispatches that can occur before using the long delay
+ private static final int DISPATCH_THRESHOLD = 3;
+
+ // time to wait for messages to be queued
+ private static final long WAIT_DELAY = 100;
+
+ private String jobName;
+
+ /**
+ * General event class. The type is specific to subclasses.
+ */
+ public static class Event {
+ private int type;
+ public Event(int type) {
+ this.type = type;
+ }
+ public int getType() {
+ return type;
+ }
+ @Override
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("Background Event: "); //$NON-NLS-1$
+ buffer.append(getTypeString());
+ return buffer.toString();
+ }
+ public IResource getResource() {
+ return null;
+ }
+ protected String getTypeString() {
+ return String.valueOf(type);
+ }
+ }
+
+ /**
+ * Resource event class. The type is specific to subclasses.
+ */
+ public static class ResourceEvent extends Event {
+ private IResource resource;
+ private int depth;
+ public ResourceEvent(IResource resource, int type, int depth) {
+ super(type);
+ this.resource = resource;
+ this.depth = depth;
+ }
+ public int getDepth() {
+ return depth;
+ }
+ @Override
+ public IResource getResource() {
+ return resource;
+ }
+ @Override
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("resource: "); //$NON-NLS-1$
+ buffer.append(resource.getFullPath());
+ buffer.append(" type: "); //$NON-NLS-1$
+ buffer.append(getTypeString());
+ buffer.append(" depth: "); //$NON-NLS-1$
+ buffer.append(getDepthString());
+ return buffer.toString();
+ }
+ protected String getDepthString() {
+ switch (depth) {
+ case IResource.DEPTH_ZERO :
+ return "DEPTH_ZERO"; //$NON-NLS-1$
+ case IResource.DEPTH_ONE :
+ return "DEPTH_ONE"; //$NON-NLS-1$
+ case IResource.DEPTH_INFINITE :
+ return "DEPTH_INFINITE"; //$NON-NLS-1$
+ default :
+ return "INVALID"; //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * This is a special event used to run some work in the background.
+ * The preemptive flag is used to indicate that the runnable should take
+ * the highest priority and thus be placed on the front of the queue
+ * and be processed as soon as possible, preempting any event that is currently
+ * being processed. The current event will continue processing once the
+ * high priority event has been processed
+ */
+ public static class RunnableEvent extends Event {
+ private IWorkspaceRunnable runnable;
+ private boolean preemtive;
+ public RunnableEvent(IWorkspaceRunnable runnable, boolean preemtive) {
+ super(RUNNABLE_EVENT);
+ this.runnable = runnable;
+ this.preemtive = preemtive;
+ }
+ public void run(IProgressMonitor monitor) throws CoreException {
+ runnable.run(monitor);
+ }
+ public boolean isPreemtive() {
+ return preemtive;
+ }
+ }
+
+ protected BackgroundEventHandler(String jobName, String errorTitle) {
+ this.jobName = jobName;
+ errors =
+ new ExceptionCollector(
+ errorTitle,
+ TeamPlugin.ID,
+ IStatus.ERROR,
+ null /* don't log */
+ );
+ createEventHandlingJob();
+ schedule();
+ }
+
+ /**
+ * Create the job used for processing the events in the queue. The job stops working when
+ * the queue is empty.
+ */
+ protected void createEventHandlingJob() {
+ eventHandlerJob = new Job(getName()) {
+ @Override
+ public IStatus run(IProgressMonitor monitor) {
+ return processEvents(monitor);
+ }
+ @Override
+ public boolean shouldRun() {
+ return ! isQueueEmpty();
+ }
+ @Override
+ public boolean shouldSchedule() {
+ return ! isQueueEmpty();
+ }
+ @Override
+ public boolean belongsTo(Object family) {
+ return BackgroundEventHandler.this.belongsTo(family);
+ }
+ };
+ eventHandlerJob.addJobChangeListener(new JobChangeAdapter() {
+ @Override
+ public void done(IJobChangeEvent event) {
+ jobDone(event);
+ }
+ });
+ eventHandlerJob.setSystem(true);
+ eventHandlerJob.setPriority(Job.SHORT);
+ }
+
+ /**
+ * Return whether this background handler belongs to the given job family.
+ * @param family the job family
+ * @return whether this background handler belongs to the given job family.
+ * @see Job#belongsTo(Object)
+ */
+ protected boolean belongsTo(Object family) {
+ return getJobFamiliy() == family;
+ }
+
+ /**
+ * Return the family that the background job for this
+ * event handler belongs to.
+ * @return the family that the background job for this
+ * event handler belongs to
+ */
+ protected Object getJobFamiliy() {
+ return null;
+ }
+
+ /**
+ * This method is invoked when the processing job completes. The
+ * default behavior of the handler is to restart the job if the queue
+ * is no longer empty and to clear the queue if the handler was shut down.
+ */
+ protected void jobDone(IJobChangeEvent event) {
+ if (isShutdown()) {
+ // The handler has been shutdown. Clean up the queue.
+ synchronized(this) {
+ awaitingProcessing.clear();
+ }
+ } else if (! isQueueEmpty()) {
+ // An event squeaked in as the job was finishing. Reschedule the job.
+ schedule();
+ }
+ }
+
+ /**
+ * Schedule the job to process the events now.
+ */
+ protected void schedule() {
+ eventHandlerJob.schedule();
+ }
+
+ /**
+ * Shutdown the event handler. Any events on the queue will be removed from the queue
+ * and will not be processed.
+ */
+ public void shutdown() {
+ shutdown = true;
+ eventHandlerJob.cancel();
+ }
+
+ /**
+ * Returns whether the handle has been shutdown.
+ * @return Returns whether the handle has been shutdown.
+ */
+ public boolean isShutdown() {
+ return shutdown;
+ }
+
+ /**
+ * Queue the event and start the job if it's not already doing work. If the job is
+ * already running then notify in case it was waiting.
+ * @param event the event to be queued
+ */
+ protected synchronized void queueEvent(Event event, boolean front) {
+ if (Policy.DEBUG_BACKGROUND_EVENTS) {
+ System.out.println("Event queued on " + getName() + ":" + event.toString()); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ if (front) {
+ awaitingProcessing.add(0, event);
+ } else {
+ awaitingProcessing.add(event);
+ }
+ if (!isShutdown() && eventHandlerJob != null) {
+ if(eventHandlerJob.getState() == Job.NONE) {
+ schedule();
+ } else {
+ notify();
+ }
+ }
+ }
+
+ /**
+ * Return the name that is to be associated with the background job.
+ * @return the job name
+ */
+ protected String getName() {
+ return jobName;
+ }
+
+ /*
+ * Return the next event that has been queued, removing it from the queue.
+ * @return the next event in the queue
+ */
+ protected synchronized Event nextElement() {
+ if (isShutdown() || isQueueEmpty()) {
+ return null;
+ }
+ return awaitingProcessing.remove(0);
+ }
+
+ protected synchronized Event peek() {
+ if (isShutdown() || isQueueEmpty()) {
+ return null;
+ }
+ return awaitingProcessing.get(0);
+ }
+
+ /**
+ * Return whether there are unprocessed events on the event queue.
+ * @return whether there are unprocessed events on the queue
+ */
+ protected synchronized boolean isQueueEmpty() {
+ return awaitingProcessing.isEmpty();
+ }
+
+ /**
+ * Process events from the events queue and dispatch results. This method does not
+ * directly check for or handle cancelation of the provided monitor. However,
+ * it does invoke <code>processEvent(Event)</code> which may check for and handle
+ * cancelation by shutting down the receiver.
+ * <p>
+ * The <code>isReadyForDispatch()</code> method is used in conjunction
+ * with the <code>dispatchEvents(IProgressMonitor)</code> to allow
+ * the output of the event handler to be batched in order to avoid
+ * fine grained UI updating.
+ * @param monitor a progress monitor
+ */
+ protected IStatus processEvents(IProgressMonitor monitor) {
+ errors.clear();
+ try {
+ // It's hard to know how much work is going to happen
+ // since the queue can grow. Use the current queue size as a hint to
+ // an infinite progress monitor
+ monitor.beginTask(null, IProgressMonitor.UNKNOWN);
+ IProgressMonitor subMonitor = Policy.infiniteSubMonitorFor(monitor, 90);
+ subMonitor.beginTask(null, 1024);
+
+ Event event;
+ timeOfLastDispatch = System.currentTimeMillis();
+ dispatchCount = 1;
+ while ((event = nextElement()) != null && ! isShutdown()) {
+ try {
+ processEvent(event, subMonitor);
+ if (Policy.DEBUG_BACKGROUND_EVENTS) {
+ System.out.println("Event processed on " + getName() + ":" + event.toString()); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ if(isReadyForDispatch(true /*wait if queue is empty*/)) {
+ dispatchEvents(Policy.subMonitorFor(subMonitor, 1));
+ }
+ } catch (CoreException e) {
+ // handle exception but keep going
+ handleException(e);
+ }
+ }
+ } finally {
+ monitor.done();
+ }
+ return errors.getStatus();
+ }
+
+ /**
+ * Dispatch any accumulated events by invoking <code>doDispatchEvents</code>
+ * and then rest the dispatch counters.
+ * @param monitor a progress monitor
+ * @throws TeamException
+ */
+ protected final void dispatchEvents(IProgressMonitor monitor) throws TeamException {
+ if (doDispatchEvents(monitor)) {
+ // something was dispatched so adjust dispatch count.
+ dispatchCount++;
+ }
+ timeOfLastDispatch = System.currentTimeMillis();
+ }
+
+ /**
+ * Notify clients of processed events. Return <code>true</code> if there
+ * was something to dispatch and false otherwise. This is used to help
+ * control the frequency of dispatches (e.g. if there is a lot of dispatching
+ * going on, the frequency of dispatches may be reduced.
+ * @param monitor a progress monitor
+ */
+ protected abstract boolean doDispatchEvents(IProgressMonitor monitor) throws TeamException;
+
+ /**
+ * Returns <code>true</code> if processed events should be dispatched and
+ * <code>false</code> otherwise. Events are dispatched at regular intervals
+ * to avoid fine grain events causing the UI to be too jumpy. Also, if the
+ * events queue is empty we will wait a small amount of time to allow
+ * pending events to be queued. The queueEvent notifies when events are
+ * queued.
+ * @return <code>true</code> if processed events should be dispatched and
+ * <code>false</code> otherwise
+ */
+ protected boolean isReadyForDispatch(boolean wait) {
+ // Check if the time since the last dispatch is greater than the delay.
+ if (isDispatchDelayExceeded())
+ return true;
+
+ synchronized(this) {
+ // If we have incoming events, process them before dispatching
+ if(! isQueueEmpty() || ! wait) {
+ return false;
+ }
+ // There are no incoming events but we want to wait a little before
+ // dispatching in case more events come in.
+ try {
+ wait(getDispatchWaitDelay());
+ } catch (InterruptedException e) {
+ // just continue
+ }
+ }
+ return isQueueEmpty() || isDispatchDelayExceeded();
+ }
+
+ private boolean isDispatchDelayExceeded() {
+ long duration = System.currentTimeMillis() - timeOfLastDispatch;
+ return ((dispatchCount < DISPATCH_THRESHOLD && duration >= getShortDispatchDelay()) ||
+ duration >= getLongDispatchDelay());
+ }
+
+ /**
+ * Return the amount of time to wait for more events before dispatching.
+ * @return the amount of time to wait for more events before dispatching.
+ */
+ protected long getDispatchWaitDelay() {
+ return WAIT_DELAY;
+ }
+
+ /**
+ * Return the value that is used to determine how often
+ * the events are dispatched (i.e. how often the UI is
+ * updated) for the first 3 cycles. The default value is 1.5 seconds.
+ * After the first 3 cycles, a longer delay is used
+ * @return the dispatch delay used for the first 3 cycles.
+ */
+ protected long getShortDispatchDelay() {
+ return DISPATCH_DELAY;
+ }
+
+ /**
+ * Return the value that is used to determine how often
+ * the events are dispatched (i.e. how often the UI is
+ * updated) after the first 3 cycles. The default value is 10 seconds.
+ * @return the dispatch delay used after the first 3 cycles.
+ */
+ protected long getLongDispatchDelay() {
+ return LONG_DISPATCH_DELAY;
+ }
+
+ /**
+ * Handle the exception by recording it in the errors list.
+ * @param e
+ */
+ protected void handleException(CoreException e) {
+ errors.handleException(e);
+ }
+
+ /**
+ * Process the event in the context of a running background job. Subclasses may
+ * (but are not required to) check the provided monitor for cancelation and shut down the
+ * receiver by invoking the <code>shutdown()</code> method.
+ * <p>
+ * In many cases, a background event handler will translate incoming events into outgoing
+ * events. If this is the case, the handler should accumulate these events in the
+ * <code>proceessEvent</code> method and propagate them from the <code>dispatchEvent</code>
+ * method which is invoked periodically in order to batch outgoing events and avoid
+ * the UI becoming too jumpy.
+ *
+ * @param event the <code>Event</code> to be processed
+ * @param monitor a progress monitor
+ */
+ protected abstract void processEvent(Event event, IProgressMonitor monitor) throws CoreException;
+
+ /**
+ * Return the job from which the <code>processedEvent</code> method is invoked.
+ * @return Returns the background event handling job.
+ */
+ public Job getEventHandlerJob() {
+ return eventHandlerJob;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Cache.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Cache.java
new file mode 100644
index 000000000..b01abf667
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Cache.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.ICache;
+import org.eclipse.team.core.ICacheListener;
+
+/**
+ * A synchronize operation context that supports caching of
+ * properties relevant to the operation and the registering of
+ * dispose listeners.
+ *
+ * @see org.eclipse.team.core.ICache
+ * @since 3.2
+ */
+public class Cache implements ICache {
+
+ Map<String, Object> properties;
+ ListenerList<ICacheListener> listeners;
+
+ @Override
+ public synchronized void put(String name, Object value) {
+ if (properties == null) {
+ properties = new HashMap<>();
+ }
+ properties.put(name, value);
+ }
+
+ @Override
+ public synchronized Object get(String name) {
+ if (properties == null)
+ return null;
+ return properties.get(name);
+ }
+
+ @Override
+ public synchronized void remove(String name) {
+ if (properties != null)
+ properties.remove(name);
+ if (properties.isEmpty()) {
+ properties = null;
+ }
+
+ }
+
+ @Override
+ public synchronized void addCacheListener(ICacheListener listener) {
+ if (listeners == null)
+ listeners = new ListenerList<>(ListenerList.IDENTITY);
+ listeners.add(listener);
+
+ }
+
+ @Override
+ public synchronized void removeDisposeListener(ICacheListener listener) {
+ removeCacheListener(listener);
+ }
+
+ @Override
+ public synchronized void removeCacheListener(ICacheListener listener) {
+ if (listeners != null)
+ listeners.remove(listener);
+ }
+
+ public void dispose() {
+ if (listeners != null) {
+ for (ICacheListener listener : listeners) {
+ SafeRunner.run(new ISafeRunnable(){
+ @Override
+ public void run() throws Exception {
+ listener.cacheDisposed(Cache.this);
+ }
+ @Override
+ public void handleException(Throwable exception) {
+ // Ignore since the platform logs the error
+
+ }
+ });
+ }
+ }
+ properties = null;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultFileModificationValidator.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultFileModificationValidator.java
new file mode 100644
index 000000000..fd911f7e9
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultFileModificationValidator.java
@@ -0,0 +1,123 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.team.FileModificationValidationContext;
+import org.eclipse.core.resources.team.FileModificationValidator;
+import org.eclipse.core.runtime.*;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.ITeamStatus;
+import org.eclipse.team.core.TeamStatus;
+
+public class DefaultFileModificationValidator extends FileModificationValidator {
+
+ /*
+ * A validator plugged in the the Team UI that will prompt
+ * the user to make read-only files writable. In the absence of
+ * this validator, edit/save fail on read-only files.
+ */
+ private FileModificationValidator uiValidator;
+
+ protected IStatus getDefaultStatus(IFile file) {
+ return
+ file.isReadOnly()
+ ? new TeamStatus(IStatus.ERROR, TeamPlugin.ID, ITeamStatus.READ_ONLY_LOCAL, NLS.bind(Messages.FileModificationValidator_fileIsReadOnly, new String[] { file.getFullPath().toString() }), null, file)
+ : Status.OK_STATUS;
+ }
+
+ @Override
+ public IStatus validateEdit(IFile[] files, FileModificationValidationContext context) {
+ IFile[] readOnlyFiles = getReadOnly(files);
+ if (readOnlyFiles.length == 0)
+ return Status.OK_STATUS;
+ synchronized (this) {
+ if (uiValidator == null)
+ uiValidator = loadUIValidator();
+ }
+ if (uiValidator != null) {
+ return uiValidator.validateEdit(files, context);
+ }
+ // There was no plugged in validator so fail gracefully
+ return getStatus(files);
+ }
+
+ protected IStatus getStatus(IFile[] files) {
+ if (files.length == 1) {
+ return getDefaultStatus(files[0]);
+ }
+
+ IStatus[] stati = new Status[files.length];
+ boolean allOK = true;
+
+ for (int i = 0; i < files.length; i++) {
+ stati[i] = getDefaultStatus(files[i]);
+ if(! stati[i].isOK())
+ allOK = false;
+ }
+
+ return new MultiStatus(TeamPlugin.ID,
+ 0, stati,
+ allOK
+ ? Messages.ok
+ : Messages.FileModificationValidator_someReadOnly,
+ null);
+ }
+
+ private IFile[] getReadOnly(IFile[] files) {
+ List<IFile> result = new ArrayList<>(files.length);
+ for (int i = 0; i < files.length; i++) {
+ IFile file = files[i];
+ if (file.isReadOnly()) {
+ result.add(file);
+ }
+ }
+ return result.toArray(new IFile[result.size()]);
+ }
+
+ @Override
+ public IStatus validateSave(IFile file) {
+ if (!file.isReadOnly())
+ return Status.OK_STATUS;
+ synchronized (this) {
+ if (uiValidator == null)
+ uiValidator = loadUIValidator();
+ }
+ if (uiValidator != null) {
+ return uiValidator.validateSave(file);
+ }
+ return getDefaultStatus(file);
+ }
+
+ private FileModificationValidator loadUIValidator() {
+ IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint(TeamPlugin.ID, TeamPlugin.DEFAULT_FILE_MODIFICATION_VALIDATOR_EXTENSION);
+ if (extension != null) {
+ IExtension[] extensions = extension.getExtensions();
+ if (extensions.length > 0) {
+ IConfigurationElement[] configElements = extensions[0].getConfigurationElements();
+ if (configElements.length > 0) {
+ try {
+ Object o = configElements[0].createExecutableExtension("class"); //$NON-NLS-1$
+ if (o instanceof FileModificationValidator) {
+ return (FileModificationValidator)o;
+ }
+ } catch (CoreException e) {
+ TeamPlugin.log(e);
+ }
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultMoveDeleteHook.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultMoveDeleteHook.java
new file mode 100644
index 000000000..a14410124
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultMoveDeleteHook.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.team.IMoveDeleteHook;
+import org.eclipse.core.resources.team.IResourceTree;
+import org.eclipse.core.runtime.IProgressMonitor;
+
+/**
+ * Internal class which provides the default behavior for resource deletions and moves.
+ *
+ */
+
+public class DefaultMoveDeleteHook implements IMoveDeleteHook {
+
+ @Override
+ public boolean deleteFile(
+ IResourceTree tree,
+ IFile file,
+ int updateFlags,
+ IProgressMonitor monitor) {
+ return false;
+ }
+
+ @Override
+ public boolean deleteFolder(
+ IResourceTree tree,
+ IFolder folder,
+ int updateFlags,
+ IProgressMonitor monitor) {
+ return false;
+ }
+
+ @Override
+ public boolean deleteProject(
+ IResourceTree tree,
+ IProject project,
+ int updateFlags,
+ IProgressMonitor monitor) {
+ return false;
+ }
+
+ @Override
+ public boolean moveFile(
+ IResourceTree tree,
+ IFile source,
+ IFile destination,
+ int updateFlags,
+ IProgressMonitor monitor) {
+ return false;
+ }
+
+ @Override
+ public boolean moveFolder(
+ IResourceTree tree,
+ IFolder source,
+ IFolder destination,
+ int updateFlags,
+ IProgressMonitor monitor) {
+ return false;
+ }
+
+ @Override
+ public boolean moveProject(
+ IResourceTree tree,
+ IProject source,
+ IProjectDescription description,
+ int updateFlags,
+ IProgressMonitor monitor) {
+ return false;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultProjectSetCapability.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultProjectSetCapability.java
new file mode 100644
index 000000000..ff9105342
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/DefaultProjectSetCapability.java
@@ -0,0 +1,24 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2006 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:
+ * Dan Rubel - initial API and implementation
+ * IBM Corporation - ongoing maintenance
+ *******************************************************************************/
+package org.eclipse.team.internal.core;
+
+import org.eclipse.team.core.ProjectSetCapability;
+
+/**
+ * An internal class for backward compatibility with the
+ * {@link org.eclipse.team.core.IProjectSetSerializer} interface.
+ *
+ * @since 3.0
+ */
+public class DefaultProjectSetCapability extends ProjectSetCapability {
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ExceptionCollector.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ExceptionCollector.java
new file mode 100644
index 000000000..e56522730
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ExceptionCollector.java
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.util.*;
+
+import org.eclipse.core.runtime.*;
+
+/**
+ * Collects exceptions and can be configured to ignore duplicates exceptions. Exceptions can be logged
+ * and a MultiStatus containing all collected exceptions can be returned.
+ *
+ * @see org.eclipse.core.runtime.MultiStatus
+ * @see org.eclipse.core.runtime.IStatus
+ *
+ * @since 3.0
+ */
+public class ExceptionCollector {
+
+ private List<IStatus> statuses = new ArrayList<>();
+ private String message;
+ private String pluginId;
+ private int severity;
+ private ILog log;
+
+ /**
+ * Creates a collector and initializes the parameters for the top-level exception
+ * that would be returned from <code>getStatus</code> is exceptions are collected.
+ *
+ * @param message a human-readable message, localized to the current locale
+ * @param pluginId the unique identifier of the relevant plug-in
+ * @param severity the severity; one of <code>OK</code>,
+ * <code>ERROR</code>, <code>INFO</code>, or <code>WARNING</code>
+ * @param log the log to output the exceptions to, or <code>null</code> if
+ * exceptions should not be logged.
+ */
+ public ExceptionCollector(String message, String pluginId, int severity, ILog log) {
+ this.message = message;
+ this.pluginId = pluginId;
+ this.severity = severity;
+ this.log = log;
+ }
+
+ /**
+ * Clears the exceptions collected.
+ */
+ public void clear() {
+ statuses.clear();
+ }
+
+ /**
+ * Returns a status that represents the exceptions collected. If the collector
+ * is empty <code>IStatus.OK</code> is returned. Otherwise a MultiStatus containing
+ * all collected exceptions is returned.
+ * @return a multistatus containing the exceptions collected or IStatus.OK if
+ * the collector is empty.
+ */
+ public IStatus getStatus() {
+ if(statuses.isEmpty()) {
+ return Status.OK_STATUS;
+ } else {
+ MultiStatus multiStatus = new MultiStatus(pluginId, severity, message, null);
+ Iterator it = statuses.iterator();
+ while (it.hasNext()) {
+ IStatus status = (IStatus) it.next();
+ multiStatus.merge(status);
+ }
+ return multiStatus;
+ }
+ }
+
+ /**
+ * Add this exception to the collector. If a log was specified in the constructor
+ * then the exception will be output to the log. You can retreive exceptions
+ * using <code>getStatus</code>.
+ *
+ * @param exception the exception to collect
+ */
+ public void handleException(CoreException exception) {
+ // log the exception if we have a log
+ if(log != null) {
+ log.log(new Status(severity, pluginId, 0, message, exception));
+ }
+ // Record each status individually to flatten the resulting multi-status
+ IStatus exceptionStatus = exception.getStatus();
+ // Wrap the exception so the stack trace is not lost.
+ IStatus status = new Status(exceptionStatus.getSeverity(), exceptionStatus.getPlugin(), exceptionStatus.getCode(), exceptionStatus.getMessage(), exception);
+ recordStatus(status);
+ IStatus[] children = status.getChildren();
+ for (int i = 0; i < children.length; i++) {
+ IStatus status2 = children[i];
+ recordStatus(status2);
+ }
+ }
+
+ private void recordStatus(IStatus status) {
+ statuses.add(status);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/FileContentManager.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/FileContentManager.java
new file mode 100644
index 000000000..d3ee63028
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/FileContentManager.java
@@ -0,0 +1,260 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.io.*;
+import java.util.*;
+
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.core.runtime.content.IContentTypeManager;
+import org.eclipse.team.core.*;
+
+/**
+ * TODO: implement extension point
+ */
+public class FileContentManager implements IFileContentManager {
+
+ private static final String PREF_TEAM_EXTENSION_TYPES= "file_types"; //$NON-NLS-1$
+ private static final String PREF_TEAM_FILENAME_TYPES= "cvs_mode_for_file_without_extensions"; //$NON-NLS-1$
+
+ private static class StringMapping implements IStringMapping {
+
+ private final String fString;
+ private final int fType;
+
+ public StringMapping(String string, int type) {
+ fString= string;
+ fType= type;
+ }
+
+ @Override
+ public String getString() {
+ return fString;
+ }
+
+ @Override
+ public int getType() {
+ return fType;
+ }
+ }
+
+ private static class UserExtensionMappings extends UserStringMappings {
+
+ public UserExtensionMappings(String key) {
+ super(key);
+ }
+
+ @Override
+ protected Map<String, Integer> loadMappingsFromPreferences() {
+ final Map<String, Integer> result= super.loadMappingsFromPreferences();
+ if (loadMappingsFromOldWorkspace(result)) {
+ TeamPlugin.getPlugin().savePluginPreferences();
+ }
+ return result;
+ }
+
+ /**
+ * If the workspace is an old 2.0 one, read the old file and delete it.
+ *
+ * @param A map where the new mappings should be added.
+ *
+ * @return true if the workspace was a 2.0 one and the old mappings have
+ * been added to the map, false otherwise.
+ *
+ */
+ private boolean loadMappingsFromOldWorkspace(Map<String, Integer> map) {
+ // File name of the persisted file type information
+ String STATE_FILE = ".fileTypes"; //$NON-NLS-1$
+ IPath pluginStateLocation = TeamPlugin.getPlugin().getStateLocation().append(STATE_FILE);
+ File f = pluginStateLocation.toFile();
+
+ if (!f.exists())
+ return false;
+
+ try {
+ DataInputStream input = new DataInputStream(new FileInputStream(f));
+ try {
+ map.putAll(readOldFormatExtensionMappings(input));
+ } finally {
+ input.close();
+ f.delete();
+ }
+ } catch (IOException ex) {
+ TeamPlugin.log(IStatus.ERROR, ex.getMessage(), ex);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Read the saved file type state from the given input stream.
+ *
+ * @param input the input stream to read the saved state from
+ * @throws IOException if an I/O problem occurs
+ */
+ private Map<String, Integer> readOldFormatExtensionMappings(DataInputStream input) throws IOException {
+ final Map<String, Integer> result= new TreeMap<>();
+ int numberOfMappings = 0;
+ try {
+ numberOfMappings = input.readInt();
+ } catch (EOFException e) {
+ // Ignore the exception, it will occur if there are no
+ // patterns stored in the state file.
+ return Collections.emptyMap();
+ }
+ for (int i = 0; i < numberOfMappings; i++) {
+ final String extension = input.readUTF();
+ final int type = input.readInt();
+ result.put(extension, Integer.valueOf(type));
+ }
+ return result;
+ }
+ }
+
+ private final UserStringMappings fUserExtensionMappings, fUserNameMappings;
+ private PluginStringMappings fPluginExtensionMappings;//, fPluginNameMappings;
+ private IContentType textContentType;
+
+ public FileContentManager() {
+ fUserExtensionMappings= new UserExtensionMappings(PREF_TEAM_EXTENSION_TYPES);
+ fUserNameMappings= new UserStringMappings(PREF_TEAM_FILENAME_TYPES);
+ fPluginExtensionMappings= new PluginStringMappings(TeamPlugin.FILE_TYPES_EXTENSION, "extension"); //$NON-NLS-1$
+ }
+
+ @Override
+ public int getTypeForName(String filename) {
+ final int userType= fUserNameMappings.getType(filename);
+// final int pluginType= fPluginNameMappings.getType(filename);
+// return userType != Team.UNKNOWN ? userType : pluginType;
+ return userType;
+ }
+
+ @Override
+ public int getTypeForExtension(String extension) {
+ final int userType= fUserExtensionMappings.getType(extension);
+ final int pluginType= fPluginExtensionMappings.getType(extension);
+ return userType != Team.UNKNOWN ? userType : pluginType;
+ }
+
+ @Override
+ public void addNameMappings(String[] names, int [] types) {
+ fUserNameMappings.addStringMappings(names, types);
+ }
+
+ @Override
+ public void addExtensionMappings(String[] extensions, int [] types) {
+ fUserExtensionMappings.addStringMappings(extensions, types);
+ }
+
+ @Override
+ public void setNameMappings(String[] names, int [] types) {
+ fUserNameMappings.setStringMappings(names, types);
+ }
+
+ @Override
+ public void setExtensionMappings(String[] extensions, int [] types) {
+ fUserExtensionMappings.setStringMappings(extensions, types);
+ }
+
+ @Override
+ public IStringMapping[] getNameMappings() {
+ return getMappings(fUserNameMappings, null);//fPluginNameMappings);
+ }
+
+ @Override
+ public IStringMapping[] getExtensionMappings() {
+ return getMappings(fUserExtensionMappings, fPluginExtensionMappings);
+ }
+
+ @Override
+ public int getType(IStorage storage) {
+ int type;
+
+ final String name= storage.getName();
+ if (name != null && (type= getTypeForName(name)) != Team.UNKNOWN)
+ return type;
+
+ final String extension= getFileExtension(name);
+ if (extension != null && (type= getTypeForExtension(extension)) != Team.UNKNOWN)
+ return type;
+
+ IContentType contentType = Platform.getContentTypeManager().findContentTypeFor(name);
+ if (contentType != null) {
+ IContentType textType = getTextContentType();
+ if (contentType.isKindOf(textType)) {
+ return Team.TEXT;
+ }
+ }
+
+ return Team.UNKNOWN;
+ }
+
+ private IContentType getTextContentType() {
+ if (textContentType == null)
+ textContentType = Platform.getContentTypeManager().getContentType(IContentTypeManager.CT_TEXT);
+ return textContentType;
+ }
+
+ @Override
+ public IStringMapping[] getDefaultNameMappings() {
+ // TODO: There is currently no extension point for this
+ return new IStringMapping[0];//getStringMappings(fPluginNameMappings.referenceMap());
+ }
+
+ @Override
+ public IStringMapping[] getDefaultExtensionMappings() {
+ return getStringMappings(fPluginExtensionMappings.referenceMap());
+ }
+
+ @Override
+ public boolean isKnownExtension(String extension) {
+ return fUserExtensionMappings.referenceMap().containsKey(extension)
+ || fPluginExtensionMappings.referenceMap().containsKey(extension);
+ }
+
+ @Override
+ public boolean isKnownFilename(String filename) {
+ return fUserNameMappings.referenceMap().containsKey(filename);
+// || fPluginNameMappings.referenceMap().containsKey(filename);
+ }
+
+ private static String getFileExtension(String name) {
+ if (name == null)
+ return null;
+ int index = name.lastIndexOf('.');
+ if (index == -1)
+ return null;
+ if (index == (name.length() - 1))
+ return ""; //$NON-NLS-1$
+ return name.substring(index + 1);
+ }
+
+ private static IStringMapping [] getStringMappings(Map map) {
+ final IStringMapping [] result= new IStringMapping [map.size()];
+ int index= 0;
+ for (final Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
+ final Map.Entry entry= (Map.Entry)iter.next();
+ result[index++]= new StringMapping((String)entry.getKey(), ((Integer)entry.getValue()).intValue());
+ }
+ return result;
+ }
+
+ private IStringMapping [] getMappings(UserStringMappings userMappings, PluginStringMappings pluginMappings) {
+ final Map<String, Integer> mappings= new HashMap<>();
+ if (pluginMappings != null)
+ mappings.putAll(pluginMappings.referenceMap());
+ mappings.putAll(userMappings.referenceMap());
+ return getStringMappings(mappings);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/FileModificationValidatorManager.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/FileModificationValidatorManager.java
new file mode 100644
index 000000000..b62b49bab
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/FileModificationValidatorManager.java
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+
+import java.util.*;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.team.FileModificationValidationContext;
+import org.eclipse.core.resources.team.FileModificationValidator;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.MultiStatus;
+import org.eclipse.team.core.RepositoryProvider;
+
+public class FileModificationValidatorManager extends FileModificationValidator {
+ private FileModificationValidator defaultValidator;
+
+ /*
+ * @see IFileModificationValidator#validateEdit(IFile[], Object)
+ * For all files, determine which provider.
+ * Ask each provider once for its files.
+ * Collect the resulting status' and return a MultiStatus.
+ */
+ @Override
+ public IStatus validateEdit(IFile[] files, FileModificationValidationContext context) {
+ ArrayList<IStatus> returnStati = new ArrayList<>();
+
+ //map provider to the files under that provider's control
+ Map<RepositoryProvider, List<IFile>> providersToFiles = new HashMap<>(files.length);
+
+ //for each file, determine which provider, map providers to files
+ for (int i = 0; i < files.length; i++) {
+ IFile file = files[i];
+ RepositoryProvider provider = RepositoryProvider.getProvider(file.getProject());
+
+ if (!providersToFiles.containsKey(provider)) {
+ providersToFiles.put(provider, new ArrayList<>());
+ }
+
+ providersToFiles.get(provider).add(file);
+ }
+
+ Iterator<RepositoryProvider> providersIterator = providersToFiles.keySet().iterator();
+
+ boolean allOK = true;
+
+ //for each provider, validate its files
+ while(providersIterator.hasNext()) {
+ RepositoryProvider provider = providersIterator.next();
+ List<IFile> filesList = providersToFiles.get(provider);
+ IFile[] filesArray = filesList.toArray(new IFile[filesList.size()]);
+ FileModificationValidator validator = getDefaultValidator();
+
+ //if no provider or no validator use the default validator
+ if (provider != null) {
+ FileModificationValidator v = provider.getFileModificationValidator2();
+ if (v != null) validator = v;
+ }
+
+ IStatus status = validator.validateEdit(filesArray, context);
+ if(!status.isOK())
+ allOK = false;
+
+ returnStati.add(status);
+ }
+
+ if (returnStati.size() == 1) {
+ return returnStati.get(0);
+ }
+
+ return new MultiStatus(TeamPlugin.ID, 0, returnStati.toArray(new IStatus[returnStati.size()]),
+ allOK ? Messages.ok : Messages.FileModificationValidator_editFailed, null);
+ }
+
+ @Override
+ public IStatus validateSave(IFile file) {
+ RepositoryProvider provider = RepositoryProvider.getProvider(file.getProject());
+ FileModificationValidator validator = getDefaultValidator();
+
+ //if no provider or no validator use the default validator
+ if (provider != null) {
+ FileModificationValidator v = provider.getFileModificationValidator2();
+ if (v != null) validator = v;
+ }
+
+ return validator.validateSave(file);
+ }
+
+ private synchronized FileModificationValidator getDefaultValidator() {
+ if (defaultValidator == null) {
+ defaultValidator = new DefaultFileModificationValidator();
+ }
+ return defaultValidator;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/IRepositoryProviderListener.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/IRepositoryProviderListener.java
new file mode 100644
index 000000000..bb26bd84b
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/IRepositoryProviderListener.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) 2006 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.internal.core;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.team.core.RepositoryProvider;
+
+/**
+ * Interface for listening to repository provider changes
+ */
+public interface IRepositoryProviderListener {
+ void providerMapped(RepositoryProvider provider);
+ void providerUnmapped(IProject project);
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/InfiniteSubProgressMonitor.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/InfiniteSubProgressMonitor.java
new file mode 100644
index 000000000..71a59a269
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/InfiniteSubProgressMonitor.java
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.SubProgressMonitor;
+
+/**
+ * Provides an infinite progress monitor by subdividing by half repeatedly.
+ *
+ * The ticks parameter represents the number of ticks shown in the progress dialog
+ * (or propogated up to a parent IProgressMonitor). The totalWork parameter provided
+ * in actually a hint used to determine how work is translated into ticks.
+ * The number of totalWork that can actually be worked is n*totalWork/2 where
+ * 2^n = totalWork. What this means is that if you provide a totalWork of 32 (2^5) than
+ * the maximum number of ticks is 5*32/2 = 80.
+ *
+ */
+public class InfiniteSubProgressMonitor extends SubProgressMonitor {
+
+ int totalWork;
+ int halfWay;
+ int currentIncrement;
+ int nextProgress;
+ int worked;
+
+ /**
+ * Constructor for InfiniteSubProgressMonitor.
+ * @param monitor
+ * @param ticks
+ */
+ public InfiniteSubProgressMonitor(IProgressMonitor monitor, int ticks) {
+ this(monitor, ticks, 0);
+ }
+
+ /**
+ * Constructor for InfiniteSubProgressMonitor.
+ * @param monitor
+ * @param ticks
+ * @param style
+ */
+ public InfiniteSubProgressMonitor(IProgressMonitor monitor, int ticks, int style) {
+ super(monitor, ticks, style);
+ }
+
+ @Override
+ public void beginTask(String name, int totalWork) {
+ super.beginTask(name, totalWork);
+ this.totalWork = totalWork;
+ this.halfWay = totalWork / 2;
+ this.currentIncrement = 1;
+ this.nextProgress = currentIncrement;
+ this.worked = 0;
+ }
+
+ @Override
+ public void worked(int work) {
+ if (worked >= totalWork) return;
+ if (--nextProgress <= 0) {
+ super.worked(1);
+ worked++;
+ if (worked >= halfWay) {
+ // we have passed the current halfway point, so double the
+ // increment and reset the halfway point.
+ currentIncrement *= 2;
+ halfWay += (totalWork - halfWay) / 2;
+ }
+ // reset the progress counter to another full increment
+ nextProgress = currentIncrement;
+ }
+ }
+
+ /**
+ * Don't allow clearing of the subtask. This will stop the flickering
+ * of the subtask in the progress dialogs.
+ *
+ * @see IProgressMonitor#subTask(String)
+ */
+ @Override
+ public void subTask(String name) {
+ if(name != null && ! name.equals("")) { //$NON-NLS-1$
+ super.subTask(name);
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Messages.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Messages.java
new file mode 100644
index 000000000..94ce9b34b
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Messages.java
@@ -0,0 +1,131 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2012 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 - Initial API and implementation
+ *******************************************************************************/
+package org.eclipse.team.internal.core;
+
+import org.eclipse.osgi.util.NLS;
+
+public class Messages extends NLS {
+ private static final String BUNDLE_NAME = "org.eclipse.team.internal.core.messages";//$NON-NLS-1$
+
+ public static String LocalFileHistory_RefreshLocalHistory;
+
+ public static String MergeContext_5;
+
+ public static String MergeContext_6;
+
+ public static String ok;
+ public static String concatStrings;
+
+ public static String AbstractResourceVariantTree_0;
+
+ public static String Assert_assertionFailed;
+
+ public static String FileModificationValidator_someReadOnly;
+ public static String FileModificationValidator_fileIsReadOnly;
+ public static String FileModificationValidator_editFailed;
+
+ public static String RepositoryProvider_Error_removing_nature_from_project___1;
+ public static String RepositoryProvider_couldNotInstantiateProvider;
+ public static String RepositoryProvider_No_Provider_Registered;
+ public static String RepositoryProvider_linkedResourcesExist;
+ public static String RepositoryProvider_linkedResourcesNotSupported;
+ public static String RepositoryProvider_linkedURIsExist;
+ public static String RepositoryProvider_linkedURIsNotSupported;
+ public static String RepositoryProvider_couldNotClearAfterError;
+ public static String RepositoryProvider_invalidClass;
+ public static String RepositoryProvider_toString;
+
+ public static String SubscriberDiffTreeEventHandler_0;
+
+ public static String Team_readError;
+ public static String PollingInputStream_readTimeout;
+ public static String PollingInputStream_closeTimeout;
+ public static String PollingOutputStream_writeTimeout;
+ public static String PollingOutputStream_closeTimeout;
+ public static String TimeoutOutputStream_cannotWriteToStream;
+
+ public static String RemoteSyncElement_delimit;
+ public static String RemoteSyncElement_insync;
+ public static String RemoteSyncElement_conflicting;
+ public static String RemoteSyncElement_outgoing;
+ public static String RemoteSyncElement_incoming;
+ public static String RemoteSyncElement_change;
+ public static String RemoteSyncElement_addition;
+ public static String RemoteSyncElement_deletion;
+ public static String RemoteSyncElement_manual;
+ public static String RemoteSyncElement_auto;
+
+ public static String Team_Error_loading_ignore_state_from_disk_1;
+ public static String Team_Conflict_occured_for_ignored_resources_pattern;
+
+ public static String RemoteContentsCache_cacheDisposed;
+ public static String RemoteContentsCache_fileError;
+ public static String SubscriberEventHandler_2;
+ public static String SubscriberEventHandler_jobName;
+ public static String SubscriberChangeSetCollector_0;
+ public static String SubscriberChangeSetCollector_1;
+ public static String SubscriberChangeSetCollector_2;
+ public static String SubscriberChangeSetCollector_3;
+ public static String SubscriberChangeSetCollector_4;
+ public static String SubscriberChangeSetCollector_5;
+ public static String SubscriberEventHandler_errors;
+ public static String RemoteContentsCacheEntry_3;
+ public static String SynchronizationCacheRefreshOperation_0;
+ public static String SubscriberEventHandler_8;
+ public static String SubscriberEventHandler_9;
+ public static String SubscriberEventHandler_10;
+ public static String SubscriberEventHandler_11;
+ public static String CachedResourceVariant_0;
+ public static String CachedResourceVariant_1;
+ public static String SyncInfoTree_0;
+ public static String ResourceVariantTreeSubscriber_1;
+ public static String ResourceVariantTreeSubscriber_2;
+ public static String ResourceVariantTreeSubscriber_3;
+ public static String ResourceVariantTreeSubscriber_4;
+ public static String SyncByteConverter_1;
+ public static String BatchingLock_11;
+ public static String SubscriberEventHandler_12;
+ public static String ProjectSetCapability_0;
+ public static String ProjectSetCapability_1;
+
+ public static String SubscriberResourceMappingContext_0;
+ public static String SubscriberResourceMappingContext_1;
+ public static String MergeContext_0;
+ public static String MergeContext_1;
+ public static String MergeContext_2;
+ public static String MergeContext_3;
+ public static String MergeContext_4;
+
+ public static String LocalFileRevision_currentVersion;
+ public static String LocalFileRevision_currentVersionTag;
+ public static String LocalFileRevision_localRevisionTag;
+ static {
+ // load message values from bundle file
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+
+ public static String DelegatingStorageMerger_0;
+
+ public static String DelegatingStorageMerger_1;
+
+ public static String WorkspaceSubscriber_0;
+
+ public static String WorkspaceSubscriber_1;
+
+ public static String ScopeManagerEventHandler_0;
+
+ public static String ScopeManagerEventHandler_1;
+
+ public static String TextAutoMerge_inputEncodingError;
+ public static String TextAutoMerge_conflict;
+ public static String TextAutoMerge_outputEncodingError;
+ public static String TextAutoMerge_outputIOError;
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/MoveDeleteManager.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/MoveDeleteManager.java
new file mode 100644
index 000000000..7a8879ff4
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/MoveDeleteManager.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.team.IMoveDeleteHook;
+import org.eclipse.core.resources.team.IResourceTree;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.RepositoryProvider;
+
+public class MoveDeleteManager implements IMoveDeleteHook {
+
+ private static final IMoveDeleteHook DEFAULT_HOOK = new DefaultMoveDeleteHook();
+
+ private IMoveDeleteHook getHookFor(IResource resource) {
+ IProject project = resource.getProject();
+ RepositoryProvider provider = RepositoryProvider.getProvider(project);
+ if(provider==null) {
+ return DEFAULT_HOOK;
+ }
+ IMoveDeleteHook hook = provider.getMoveDeleteHook();
+ if (hook == null) {
+ return DEFAULT_HOOK;
+ }
+ return hook;
+ }
+
+ @Override
+ public boolean deleteFile(
+ IResourceTree tree,
+ IFile file,
+ int updateFlags,
+ IProgressMonitor monitor) {
+
+ return getHookFor(file).deleteFile(tree, file, updateFlags, monitor);
+ }
+
+ @Override
+ public boolean deleteFolder(
+ IResourceTree tree,
+ IFolder folder,
+ int updateFlags,
+ IProgressMonitor monitor) {
+
+ return getHookFor(folder).deleteFolder(tree, folder, updateFlags, monitor);
+ }
+
+ @Override
+ public boolean deleteProject(
+ IResourceTree tree,
+ IProject project,
+ int updateFlags,
+ IProgressMonitor monitor) {
+
+ return getHookFor(project).deleteProject(tree, project, updateFlags, monitor);
+ }
+
+ @Override
+ public boolean moveFile(
+ IResourceTree tree,
+ IFile source,
+ IFile destination,
+ int updateFlags,
+ IProgressMonitor monitor) {
+
+ return getHookFor(source).moveFile(tree, source, destination, updateFlags, monitor);
+ }
+
+ @Override
+ public boolean moveFolder(
+ IResourceTree tree,
+ IFolder source,
+ IFolder destination,
+ int updateFlags,
+ IProgressMonitor monitor) {
+
+ return getHookFor(source).moveFolder(tree, source, destination, updateFlags, monitor);
+ }
+
+ @Override
+ public boolean moveProject(
+ IResourceTree tree,
+ IProject source,
+ IProjectDescription description,
+ int updateFlags,
+ IProgressMonitor monitor) {
+
+ return getHookFor(source).moveProject(tree, source, description, updateFlags, monitor);
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/NullSubProgressMonitor.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/NullSubProgressMonitor.java
new file mode 100644
index 000000000..17f99cb2a
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/NullSubProgressMonitor.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.SubProgressMonitor;
+
+/**
+ * This sub-progress monitor can be used to ignore progress indication for
+ * methods but allow cancellation.
+ * <p>
+ * This implementation supports cancelation. The default implementations of the
+ * other methods do nothing.
+ * </p>
+ * @see SubProgressMonitor
+ */
+public class NullSubProgressMonitor extends SubProgressMonitor {
+ /**
+ * Constructor for InfiniteSubProgressMonitor.
+ * @param monitor
+ */
+ public NullSubProgressMonitor(IProgressMonitor monitor) {
+ super(monitor, 0, 0);
+ }
+
+ @Override
+ public void beginTask(String name, int totalWork) {
+ }
+
+ @Override
+ public void done() {
+ }
+
+ @Override
+ public void internalWorked(double work) {
+ }
+
+ @Override
+ public void subTask(String name) {
+ }
+
+ @Override
+ public void worked(int work) {
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/PessimisticResourceRuleFactory.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/PessimisticResourceRuleFactory.java
new file mode 100644
index 000000000..e7a3d8028
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/PessimisticResourceRuleFactory.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.resources.team.ResourceRuleFactory;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+
+/**
+ * A pessimistic rule factory used to ensure older repository providers
+ * are not broken by new scheduling rule locking. The workspace root
+ * is returned for all rules.
+ */
+public class PessimisticResourceRuleFactory extends ResourceRuleFactory {
+
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+
+ @Override
+ public ISchedulingRule copyRule(IResource source, IResource destination) {
+ return root;
+ }
+ @Override
+ public ISchedulingRule createRule(IResource resource) {
+ return root;
+ }
+ @Override
+ public ISchedulingRule deleteRule(IResource resource) {
+ return root;
+ }
+ @Override
+ public ISchedulingRule modifyRule(IResource resource) {
+ return root;
+ }
+ @Override
+ public ISchedulingRule moveRule(IResource source, IResource destination) {
+ return root;
+ }
+ @Override
+ public ISchedulingRule refreshRule(IResource resource) {
+ return root;
+ }
+ @Override
+ public ISchedulingRule validateEditRule(IResource[] resources) {
+ return root;
+ }
+
+ @Override
+ public ISchedulingRule charsetRule(IResource resource) {
+ return root;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/PluginStringMappings.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/PluginStringMappings.java
new file mode 100644
index 000000000..3394bd191
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/PluginStringMappings.java
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.util.*;
+
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.Team;
+
+/**
+ *
+ */
+public class PluginStringMappings {
+
+ private final String fExtensionID;
+ private final String fAttributeName;
+
+ private SortedMap<String, Integer> fMappings;
+
+ public PluginStringMappings(String extensionID, String stringAttributeName) {
+ fExtensionID= extensionID;
+ fAttributeName= stringAttributeName;
+ }
+
+ /**
+ * Load all the extension patterns contributed by plugins.
+ * @return a map with the patterns
+ */
+ private SortedMap<String, Integer> loadPluginPatterns() {
+
+ final SortedMap<String, Integer> result= new TreeMap<>();
+
+ final TeamPlugin plugin = TeamPlugin.getPlugin();
+ if (plugin == null)
+ return result;
+
+ final IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint(TeamPlugin.ID, fExtensionID);//TeamPlugin.FILE_TYPES_EXTENSION);
+ if (extension == null)
+ return result;
+
+ final IExtension[] extensions = extension.getExtensions();
+
+ for (int i = 0; i < extensions.length; i++) {
+ IConfigurationElement[] configElements = extensions[i].getConfigurationElements();
+
+ for (int j = 0; j < configElements.length; j++) {
+
+ final String ext = configElements[j].getAttribute(fAttributeName);//"extension");
+ final String type = configElements[j].getAttribute("type"); //$NON-NLS-1$
+ if (ext == null || type == null)
+ continue;
+
+ if (type.equals("text")) { //$NON-NLS-1$
+ result.put(ext, Integer.valueOf(Team.TEXT));
+ } else if (type.equals("binary")) { //$NON-NLS-1$
+ result.put(ext, Integer.valueOf(Team.BINARY));
+ }
+ }
+ }
+ return result;
+ }
+
+ public Map<String, Integer> referenceMap() {
+ if (fMappings == null) {
+ fMappings= loadPluginPatterns();
+ }
+ return fMappings;
+ }
+
+ public int getType(String filename) {
+ final Map<String, Integer> mappings= referenceMap();
+ return mappings.containsKey(filename) ? mappings.get(filename).intValue() : Team.UNKNOWN;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Policy.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Policy.java
new file mode 100644
index 000000000..66b4b3ac8
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/Policy.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import org.eclipse.core.runtime.*;
+import org.eclipse.osgi.service.debug.DebugOptionsListener;
+
+public class Policy {
+
+ //debug constants
+ public static boolean DEBUG = false;
+ public static boolean DEBUG_STREAMS = false;
+ public static boolean DEBUG_REFRESH_JOB = true;
+ public static boolean DEBUG_BACKGROUND_EVENTS = false;
+ public static boolean DEBUG_THREADING = false;
+
+ static final DebugOptionsListener DEBUG_OPTIONS_LISTENER = options -> {
+ DEBUG = options.getBooleanOption(TeamPlugin.ID + "/debug", false); //$NON-NLS-1$
+ DEBUG_STREAMS = DEBUG && options.getBooleanOption(TeamPlugin.ID + "/streams", false); //$NON-NLS-1$
+ DEBUG_REFRESH_JOB = DEBUG && options.getBooleanOption(TeamPlugin.ID + "/refreshjob", false); //$NON-NLS-1$
+ DEBUG_BACKGROUND_EVENTS = DEBUG && options.getBooleanOption(TeamPlugin.ID + "/backgroundevents", false); //$NON-NLS-1$
+ DEBUG_THREADING = DEBUG && options.getBooleanOption(TeamPlugin.ID + "/threading", false); //$NON-NLS-1$
+ };
+
+ /**
+ * Progress monitor helpers.
+ *
+ * @param monitor the progress monitor
+ */
+ public static void checkCanceled(IProgressMonitor monitor) {
+ if (monitor != null && monitor.isCanceled())
+ throw new OperationCanceledException();
+ }
+ public static IProgressMonitor monitorFor(IProgressMonitor monitor) {
+ if (monitor == null)
+ return new NullProgressMonitor();
+ return monitor;
+ }
+
+ public static IProgressMonitor subMonitorFor(IProgressMonitor monitor, int ticks) {
+ if (monitor == null)
+ return new NullProgressMonitor();
+ if (monitor instanceof NullProgressMonitor)
+ return monitor;
+ return new SubProgressMonitor(monitor, ticks);
+ }
+
+ public static IProgressMonitor infiniteSubMonitorFor(IProgressMonitor monitor, int ticks) {
+ if (monitor == null)
+ return new NullProgressMonitor();
+ if (monitor instanceof NullProgressMonitor)
+ return monitor;
+ return new InfiniteSubProgressMonitor(monitor, ticks);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/RepositoryProviderManager.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/RepositoryProviderManager.java
new file mode 100644
index 000000000..4dd0f75d0
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/RepositoryProviderManager.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.team.core.RepositoryProvider;
+
+public class RepositoryProviderManager implements IRepositoryProviderListener {
+
+ private static RepositoryProviderManager instance;
+ private ListenerList<IRepositoryProviderListener> listeners = new ListenerList<>();
+
+ public static synchronized RepositoryProviderManager getInstance() {
+ if (instance == null) {
+ instance = new RepositoryProviderManager();
+ }
+ return instance;
+ }
+
+ @Override
+ public void providerMapped(RepositoryProvider provider) {
+ Object[] allListeners = listeners.getListeners();
+ for (int i = 0; i < allListeners.length; i++) {
+ IRepositoryProviderListener listener = (IRepositoryProviderListener)allListeners[i];
+ listener.providerMapped(provider);
+ }
+ }
+
+ @Override
+ public void providerUnmapped(IProject project) {
+ Object[] allListeners = listeners.getListeners();
+ for (int i = 0; i < allListeners.length; i++) {
+ IRepositoryProviderListener listener = (IRepositoryProviderListener)allListeners[i];
+ listener.providerUnmapped(project);
+ }
+ }
+
+ public void addListener(IRepositoryProviderListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeListener(IRepositoryProviderListener listener) {
+ listeners.remove(listener);
+ }
+
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ResourceVariantCache.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ResourceVariantCache.java
new file mode 100644
index 000000000..ea6b7bda6
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ResourceVariantCache.java
@@ -0,0 +1,248 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.io.File;
+import java.util.*;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.jobs.ILock;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.variants.CachedResourceVariant;
+
+/**
+ * This class implements a caching facility that can be used by TeamProviders to cache contents
+ */
+public class ResourceVariantCache {
+
+ // Directory to cache file contents
+ private static final String CACHE_DIRECTORY = ".cache"; //$NON-NLS-1$
+ // Maximum lifespan of local cache file, in milliseconds
+ private static final long CACHE_FILE_LIFESPAN = 60*60*1000; // 1hr
+
+ // Map of registered caches indexed by local name of a QualifiedName
+ private static Map<String, ResourceVariantCache> caches = new HashMap<>(); // String (local name) > RemoteContentsCache
+
+ private String name;
+ private Map<String, ResourceVariantCacheEntry> cacheEntries;
+ private long lastCacheCleanup;
+ private int cacheDirSize;
+
+ // Lock used to serialize the writing of cache contents
+ private ILock lock = Job.getJobManager().newLock();
+
+ /**
+ * Enables the use of remote contents caching for the given cacheId. The cache ID must be unique.
+ * A good candidate for this ID is the plugin ID of the plugin performing the caching.
+ *
+ * @param cacheId the unique Id of the cache being enabled
+ */
+ public static synchronized void enableCaching(String cacheId) {
+ if (isCachingEnabled(cacheId)) return;
+ ResourceVariantCache cache = new ResourceVariantCache(cacheId);
+ cache.createCacheDirectory();
+ caches.put(cacheId, cache);
+ }
+
+ /**
+ * Returns whether caching has been enabled for the given Id. A cache should only be enabled once.
+ * It is conceivable that a cache be persisted over workbench invocations thus leading to a cache that
+ * is enabled on startup without intervention by the owning plugin.
+ *
+ * @param cacheId the unique Id of the cache
+ * @return true if caching for the given Id is enabled
+ */
+ public static boolean isCachingEnabled(String cacheId) {
+ return getCache(cacheId) != null;
+ }
+
+ /**
+ * Disable the cache, disposing of any file contents in the cache.
+ *
+ * @param cacheId the unique Id of the cache
+ */
+ public static void disableCache(String cacheId) {
+ ResourceVariantCache cache = getCache(cacheId);
+ if (cache == null) {
+ // There is no cache to dispose of
+ return;
+ }
+ caches.remove(cacheId);
+ cache.deleteCacheDirectory();
+ }
+
+ /**
+ * Return the cache for the given id or null if caching is not enabled for the given id.
+ * @param cacheId
+ * @return the cache
+ */
+ public static synchronized ResourceVariantCache getCache(String cacheId) {
+ return caches.get(cacheId);
+ }
+
+ public static synchronized void shutdown() {
+ String[] keys = caches.keySet().toArray(new String[caches.size()]);
+ for (int i = 0; i < keys.length; i++) {
+ String id = keys[i];
+ disableCache(id);
+ }
+ }
+
+ private ResourceVariantCache(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Return whether the cache contains an entry for the given id. Register a hit if it does.
+ * @param id the id of the cache entry
+ * @return true if there are contents cached for the id
+ */
+ public boolean hasEntry(String id) {
+ return internalGetCacheEntry(id) != null;
+ }
+
+ protected IPath getCachePath() {
+ return getStateLocation().append(CACHE_DIRECTORY).append(name);
+ }
+
+ private IPath getStateLocation() {
+ return TeamPlugin.getPlugin().getStateLocation();
+ }
+
+ private synchronized void clearOldCacheEntries() {
+ long current = new Date().getTime();
+ if ((lastCacheCleanup!=-1) && (current - lastCacheCleanup < CACHE_FILE_LIFESPAN)) return;
+ List<ResourceVariantCacheEntry> stale = new ArrayList<>();
+ for (Iterator iter = cacheEntries.values().iterator(); iter.hasNext();) {
+ ResourceVariantCacheEntry entry = (ResourceVariantCacheEntry) iter.next();
+ long lastHit = entry.getLastAccessTimeStamp();
+ if ((current - lastHit) > CACHE_FILE_LIFESPAN){
+ stale.add(entry);
+ }
+ }
+ for (ResourceVariantCacheEntry entry : stale) {
+ entry.dispose();
+ }
+ }
+
+ private synchronized void purgeFromCache(String id) {
+ ResourceVariantCacheEntry entry = cacheEntries.get(id);
+ File f = entry.getFile();
+ try {
+ deleteFile(f);
+ } catch (TeamException e) {
+ // Ignore the deletion failure.
+ // A failure only really matters when purging the directory on startup
+ }
+ cacheEntries.remove(id);
+ }
+
+ private synchronized void createCacheDirectory() {
+ IPath cacheLocation = getCachePath();
+ File file = cacheLocation.toFile();
+ if (file.exists()) {
+ try {
+ deleteFile(file);
+ } catch (TeamException e) {
+ // Check to see if were in an acceptable state
+ if (file.exists() && (!file.isDirectory() || file.listFiles().length != 0)) {
+ TeamPlugin.log(e);
+ }
+ }
+ }
+ if (! file.exists() && ! file.mkdirs()) {
+ TeamPlugin.log(new TeamException(NLS.bind(Messages.RemoteContentsCache_fileError, new String[] { file.getAbsolutePath() })));
+ }
+ cacheEntries = new HashMap<>();
+ lastCacheCleanup = -1;
+ cacheDirSize = 0;
+ }
+
+ private synchronized void deleteCacheDirectory() {
+ cacheEntries = null;
+ lastCacheCleanup = -1;
+ cacheDirSize = 0;
+ IPath cacheLocation = getCachePath();
+ File file = cacheLocation.toFile();
+ if (file.exists()) {
+ try {
+ deleteFile(file);
+ } catch (TeamException e) {
+ // Don't worry about problems deleting.
+ // The only case that matters is when the cache directory is created
+ }
+ }
+ }
+
+ private void deleteFile(File file) throws TeamException {
+ if (file.isDirectory()) {
+ File[] children = file.listFiles();
+ for (int i = 0; i < children.length; i++) {
+ deleteFile(children[i]);
+ }
+ }
+ if (! file.delete()) {
+ throw new TeamException(NLS.bind(Messages.RemoteContentsCache_fileError, new String[] { file.getAbsolutePath() }));
+ }
+ }
+
+ /**
+ * Purge the given cache entry from the cache. This method should only be invoked from
+ * an instance of ResourceVariantCacheEntry after it has set it's state to DISPOSED.
+ * @param entry
+ */
+ protected void purgeFromCache(ResourceVariantCacheEntry entry) {
+ purgeFromCache(entry.getId());
+ }
+
+ private synchronized ResourceVariantCacheEntry internalGetCacheEntry(String id) {
+ if (cacheEntries == null) {
+ // This probably means that the cache has been disposed
+ throw new IllegalStateException(NLS.bind(Messages.RemoteContentsCache_cacheDisposed, new String[] { name }));
+ }
+ ResourceVariantCacheEntry entry = cacheEntries.get(id);
+ if (entry != null) {
+ entry.registerHit();
+ }
+ return entry;
+ }
+
+ /**
+ * @param id the id that uniquely identifies the remote resource that is cached.
+ * @return the cache entry
+ */
+ public ResourceVariantCacheEntry getCacheEntry(String id) {
+ return internalGetCacheEntry(id);
+ }
+
+ public synchronized ResourceVariantCacheEntry add(String id, CachedResourceVariant resource) {
+ clearOldCacheEntries();
+ String filePath = String.valueOf(cacheDirSize++);
+ ResourceVariantCacheEntry entry = new ResourceVariantCacheEntry(this, lock, id, filePath);
+ entry.setResourceVariant(resource);
+ cacheEntries.put(id, entry);
+ return entry;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /*
+ * Method used for testing only
+ */
+ public ResourceVariantCacheEntry[] getEntries() {
+ return cacheEntries.values().toArray(new ResourceVariantCacheEntry[cacheEntries.size()]);
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ResourceVariantCacheEntry.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ResourceVariantCacheEntry.java
new file mode 100644
index 000000000..d7782e42a
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/ResourceVariantCacheEntry.java
@@ -0,0 +1,217 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.io.*;
+import java.util.Date;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.jobs.ILock;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.variants.CachedResourceVariant;
+
+/**
+ * This class provides the implementation for the ICacheEntry
+ */
+public class ResourceVariantCacheEntry {
+
+ public static final int UNINITIALIZED = 0;
+ public static final int READY = 1;
+ public static final int DISPOSED = 2;
+
+ private String id;
+ private String filePath;
+ private ResourceVariantCache cache;
+ private int state = UNINITIALIZED;
+ private long lastAccess;
+ private CachedResourceVariant resourceVariant;
+ private ILock lock;
+
+ public ResourceVariantCacheEntry(ResourceVariantCache cache, ILock lock, String id, String filePath) {
+ this.lock = lock;
+ state = UNINITIALIZED;
+ this.cache = cache;
+ this.id = id;
+ this.filePath = filePath;
+ registerHit();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.team.core.sync.ICacheEntry#getContents()
+ */
+ public InputStream getContents() throws TeamException {
+ if (state != READY) return null;
+ registerHit();
+ File ioFile = getFile();
+ try {
+ try {
+ if (ioFile.exists()) {
+ return new FileInputStream(ioFile);
+ }
+ } catch (IOException e) {
+ // Try to purge the cache and continue
+ cache.purgeFromCache(this);
+ throw e;
+ }
+ } catch (IOException e) {
+ // We will end up here if we couldn't read or delete the cache file
+ throw new TeamException(NLS.bind(Messages.RemoteContentsCache_fileError, new String[] { ioFile.getAbsolutePath() }), e);
+ }
+ // This can occur when there is no remote contents
+ return new ByteArrayInputStream(new byte[0]);
+ }
+
+ protected File getFile() {
+ return new File(cache.getCachePath().toFile(), filePath);
+ }
+
+ /**
+ * Set the contents of for this cache entry. This method supports concurrency by only allowing
+ * one cache entry to be written at a time. In the case of two concurrent writes to the same cache entry,
+ * the contents from the first write is used and the content from subsequent writes is ignored.
+ * @param stream an InputStream that provides the contents to be cached
+ * @param monitor a progress monitor
+ * @throws TeamException if the entry is DISPOSED or an I/O error occurred
+ */
+ public void setContents(InputStream stream, IProgressMonitor monitor) throws TeamException {
+ // Use a lock to only allow one write at a time
+ beginOperation();
+ try {
+ internalSetContents(stream, monitor);
+ } finally {
+ endOperation();
+ }
+ }
+
+ private void endOperation() {
+ lock.release();
+ }
+
+ private void beginOperation() {
+ lock.acquire();
+ }
+
+ private void internalSetContents(InputStream stream, IProgressMonitor monitor) throws TeamException {
+ // if the state is DISPOSED then there is a problem
+ if (state == DISPOSED) {
+ throw new TeamException(NLS.bind(Messages.RemoteContentsCacheEntry_3, new String[] { cache.getName(), id }));
+ }
+ // Otherwise, the state is UNINITIALIZED or READY so we can proceed
+ registerHit();
+ File ioFile = getFile();
+ try {
+
+ // Open the cache file for writing
+ OutputStream out;
+ try {
+ if (state == UNINITIALIZED) {
+ out = new BufferedOutputStream(new FileOutputStream(ioFile));
+ } else {
+ // If the entry is READY, the contents must have been read in another thread.
+ // We still need to red the contents but they can be ignored since presumably they are the same
+ out = new ByteArrayOutputStream();
+ }
+ } catch (FileNotFoundException e) {
+ throw new TeamException(NLS.bind(Messages.RemoteContentsCache_fileError, new String[] { ioFile.getAbsolutePath() }), e);
+ }
+
+ // Transfer the contents
+ try {
+ try {
+ byte[] buffer = new byte[1024];
+ int read;
+ while ((read = stream.read(buffer)) >= 0) {
+ Policy.checkCanceled(monitor);
+ out.write(buffer, 0, read);
+ }
+ } finally {
+ out.close();
+ }
+ } catch (IOException e) {
+ // Make sure we don't leave the cache file around as it may not have the right contents
+ cache.purgeFromCache(this);
+ throw e;
+ }
+
+ // Mark the cache entry as ready
+ state = READY;
+ } catch (IOException e) {
+ throw new TeamException(NLS.bind(Messages.RemoteContentsCache_fileError, new String[] { ioFile.getAbsolutePath() }), e);
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e1) {
+ // Ignore close errors
+ }
+ }
+
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.team.core.sync.ICacheEntry#getState()
+ */
+ public int getState() {
+ return state;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.team.core.sync.ICacheEntry#getSize()
+ */
+ public long getSize() {
+ if (state != READY) return 0;
+ File ioFile = getFile();
+ if (ioFile.exists()) {
+ return ioFile.length();
+ }
+ return 0;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.team.core.sync.ICacheEntry#getLastAccessTimeStamp()
+ */
+ public long getLastAccessTimeStamp() {
+ return lastAccess;
+ }
+
+ /**
+ * Registers a hit on this cache entry. This updates the last access timestamp.
+ * This method is intended to only be invoked from inside this class or the cache itself.
+ * Other clients should not use it.
+ */
+ protected void registerHit() {
+ lastAccess = new Date().getTime();
+ }
+
+ public void dispose() {
+ // Use a lock to avoid changing state while another thread may be writing
+ beginOperation();
+ try {
+ state = DISPOSED;
+ cache.purgeFromCache(this);
+ } finally {
+ endOperation();
+ }
+ }
+
+
+ public String getId() {
+ return id;
+ }
+
+ public CachedResourceVariant getResourceVariant() {
+ return resourceVariant;
+ }
+
+ public void setResourceVariant(CachedResourceVariant resourceVariant) {
+ this.resourceVariant = resourceVariant;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StorageMergerDescriptor.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StorageMergerDescriptor.java
new file mode 100644
index 000000000..15e76350f
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StorageMergerDescriptor.java
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 2006 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.internal.core;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IConfigurationElement;
+import org.eclipse.team.core.mapping.IStorageMerger;
+
+/**
+ * A factory proxy for creating a StructureCreator.
+ */
+class StorageMergerDescriptor {
+
+ private final static String CLASS_ATTRIBUTE= "class"; //$NON-NLS-1$
+
+ private IConfigurationElement fElement;
+
+ /*
+ * Creates a new sorter node with the given configuration element.
+ */
+ public StorageMergerDescriptor(IConfigurationElement element) {
+ fElement= element;
+ }
+
+ /*
+ * Creates a new stream merger from this node.
+ */
+ public IStorageMerger createStreamMerger() {
+ try {
+ return (IStorageMerger)fElement.createExecutableExtension(CLASS_ATTRIBUTE);
+ } catch (CoreException ex) {
+ //ExceptionHandler.handle(ex, SearchMessages.getString("Search.Error.createSorter.title"), SearchMessages.getString("Search.Error.createSorter.message")); //$NON-NLS-2$ //$NON-NLS-1$
+ return null;
+ } catch (ClassCastException ex) {
+ //ExceptionHandler.displayMessageDialog(ex, SearchMessages.getString("Search.Error.createSorter.title"), SearchMessages.getString("Search.Error.createSorter.message")); //$NON-NLS-2$ //$NON-NLS-1$
+ return null;
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StorageMergerRegistry.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StorageMergerRegistry.java
new file mode 100644
index 000000000..caf175a45
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StorageMergerRegistry.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core;
+
+import java.util.HashMap;
+import java.util.StringTokenizer;
+
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.mapping.IStorageMerger;
+
+public class StorageMergerRegistry {
+
+ private final static String ID_ATTRIBUTE = "id"; //$NON-NLS-1$
+ private final static String EXTENSIONS_ATTRIBUTE = "extensions"; //$NON-NLS-1$
+ private final static String CONTENT_TYPE_ID_ATTRIBUTE = "contentTypeId"; //$NON-NLS-1$
+ private static final String STORAGE_MERGER_EXTENSION_POINT = "storageMergers"; //$NON-NLS-1$
+ private static final Object STORAGE_MERGER = "storageMerger"; //$NON-NLS-1$
+ private static final String CONTENT_TYPE_BINDING= "contentTypeBinding"; //$NON-NLS-1$
+ private static final String STORAGE_MERGER_ID_ATTRIBUTE= "storageMergerId"; //$NON-NLS-1$
+
+ private static boolean NORMALIZE_CASE= true;
+
+ private static StorageMergerRegistry instance;
+
+ private HashMap<String, Object> fIdMap; // maps ids to datas
+ private HashMap<String, Object> fExtensionMap; // maps extensions to datas
+ private HashMap<IContentType, Object> fContentTypeBindings; // maps content type bindings to datas
+ private boolean fRegistriesInitialized;
+
+ public static StorageMergerRegistry getInstance() {
+ if (instance == null) {
+ instance = new StorageMergerRegistry();
+ }
+ return instance;
+ }
+
+ /**
+ * Returns a stream merger for the given type.
+ *
+ * @param type the type for which to find a stream merger
+ * @return a stream merger for the given type, or <code>null</code> if no
+ * stream merger has been registered
+ */
+ public IStorageMerger createStreamMerger(String type) {
+ initializeRegistry();
+ StorageMergerDescriptor descriptor= (StorageMergerDescriptor) search(type);
+ if (descriptor != null)
+ return descriptor.createStreamMerger();
+ return null;
+ }
+
+ /**
+ * Returns a stream merger for the given content type.
+ *
+ * @param type the type for which to find a stream merger
+ * @return a stream merger for the given type, or <code>null</code> if no
+ * stream merger has been registered
+ */
+ public IStorageMerger createStreamMerger(IContentType type) {
+ initializeRegistry();
+ StorageMergerDescriptor descriptor= (StorageMergerDescriptor) search(type);
+ if (descriptor != null)
+ return descriptor.createStreamMerger();
+ return null;
+ }
+
+ private void initializeRegistry() {
+ if (!fRegistriesInitialized) {
+ registerExtensions();
+ fRegistriesInitialized= true;
+ }
+ }
+
+ /**
+ * Registers all stream mergers, structure creators, content merge viewers, and structure merge viewers
+ * that are found in the XML plugin files.
+ */
+ private void registerExtensions() {
+ IExtensionRegistry registry= Platform.getExtensionRegistry();
+
+ // collect all IStreamMergers
+ IConfigurationElement[] elements= registry.getConfigurationElementsFor(TeamPlugin.ID, STORAGE_MERGER_EXTENSION_POINT);
+ for (int i= 0; i < elements.length; i++) {
+ IConfigurationElement element= elements[i];
+ if (STORAGE_MERGER.equals(element.getName()))
+ register(element, new StorageMergerDescriptor(element));
+ else if (CONTENT_TYPE_BINDING.equals(element.getName()))
+ createBinding(element, STORAGE_MERGER_ID_ATTRIBUTE);
+ }
+ }
+
+ private static String normalizeCase(String s) {
+ if (NORMALIZE_CASE && s != null)
+ return s.toUpperCase();
+ return s;
+ }
+
+ void register(IConfigurationElement element, Object data) {
+ String id = element.getAttribute(ID_ATTRIBUTE);
+ if (id != null) {
+ if (fIdMap == null)
+ fIdMap = new HashMap<>();
+ fIdMap.put(id, data);
+ }
+
+ String types = element.getAttribute(EXTENSIONS_ATTRIBUTE);
+ if (types != null) {
+ if (fExtensionMap == null)
+ fExtensionMap = new HashMap<>();
+ StringTokenizer tokenizer = new StringTokenizer(types, ","); //$NON-NLS-1$
+ while (tokenizer.hasMoreElements()) {
+ String extension = tokenizer.nextToken().trim();
+ fExtensionMap.put(normalizeCase(extension), data);
+ }
+ }
+ }
+
+ void createBinding(IConfigurationElement element, String idAttributeName) {
+ String type = element.getAttribute(CONTENT_TYPE_ID_ATTRIBUTE);
+ String id = element.getAttribute(idAttributeName);
+ if (id == null)
+ logErrorMessage(NLS.bind("Target attribute id '{0}' missing", idAttributeName)); //$NON-NLS-1$
+ if (type != null && id != null && fIdMap != null) {
+ Object o = fIdMap.get(id);
+ if (o != null) {
+ IContentType ct = Platform.getContentTypeManager().getContentType(type);
+ if (ct != null) {
+ if (fContentTypeBindings == null)
+ fContentTypeBindings = new HashMap<>();
+ fContentTypeBindings.put(ct, o);
+ } else {
+ logErrorMessage(NLS.bind("Content type id '{0}' not found", type)); //$NON-NLS-1$
+ }
+ } else {
+ logErrorMessage(NLS.bind("Target '{0}' not found", id)); //$NON-NLS-1$$
+ }
+ }
+ }
+
+ private void logErrorMessage(String string) {
+ TeamPlugin.log(IStatus.ERROR, string, null);
+ }
+
+ Object search(IContentType type) {
+ if (fContentTypeBindings != null) {
+ for (; type != null; type = type.getBaseType()) {
+ Object data = fContentTypeBindings.get(type);
+ if (data != null)
+ return data;
+ }
+ }
+ return null;
+ }
+
+ Object search(String extension) {
+ if (fExtensionMap != null)
+ return fExtensionMap.get(normalizeCase(extension));
+ return null;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StringMatcher.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StringMatcher.java
new file mode 100644
index 000000000..5aa255d6a
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/StringMatcher.java
@@ -0,0 +1,410 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+
+import java.util.Vector;
+
+/**
+ * A string pattern matcher. Supports '*' and '?' wildcards.
+ * Note: code copied from org.eclipse.jdt.internal.core.util.StringMatcher on April 3, 2001
+ * (version 0.1 - 010901H18 [rename jbl]). Sync'ed on November 4, 2009 (revision 1.17).
+ *
+ */
+public class StringMatcher {
+ protected String fPattern;
+ protected int fLength; // pattern length
+ protected boolean fIgnoreWildCards;
+ protected boolean fIgnoreCase;
+ protected boolean fHasLeadingStar;
+ protected boolean fHasTrailingStar;
+ protected String fSegments[]; //the given pattern is split into * separated segments
+
+ /* boundary value beyond which we don't need to search in the text */
+ protected int fBound = 0;
+ private boolean fPathPattern;
+
+
+ protected static final char fSingleWildCard = '\u0000';
+
+ public static class Position {
+ int start; //inclusive
+ int end; //exclusive
+ public Position(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+ public int getStart() {
+ return start;
+ }
+ public int getEnd() {
+ return end;
+ }
+ }
+
+ /**
+ * StringMatcher constructor takes in a String object that is a simple
+ * pattern. The pattern may contain '*' for 0 and many characters and
+ * '?' for exactly one character.
+ *
+ * Literal '*' and '?' characters must be escaped in the pattern
+ * e.g., "\*" means literal "*", etc.
+ *
+ * Escaping any other character (including the escape character itself),
+ * just results in that character in the pattern.
+ * e.g., "\a" means "a" and "\\" means "\"
+ *
+ * If invoking the StringMatcher with string literals in Java, don't forget
+ * escape characters are represented by "\\".
+ *
+ * @param pattern the pattern to match text against
+ * @param ignoreCase if true, case is ignored
+ * @param ignoreWildCards if true, wild cards and their escape sequences are ignored
+ * (everything is taken literally).
+ */
+ public StringMatcher(String pattern, boolean ignoreCase, boolean ignoreWildCards) {
+ if (pattern == null)
+ throw new IllegalArgumentException();
+ fIgnoreCase = ignoreCase;
+ fIgnoreWildCards = ignoreWildCards;
+ fPattern= pattern;
+ fLength = pattern.length();
+ fPathPattern = pattern.indexOf('/') != -1;
+
+ if (fIgnoreWildCards) {
+ parseNoWildCards();
+ } else {
+ parseWildCards();
+ }
+ }
+
+ /**
+ * Find the first occurrence of the pattern between <code>start</code>(inclusive)
+ * and <code>end</code>(exclusive).
+ * @param text the String object to search in
+ * @param start the starting index of the search range, inclusive
+ * @param end the ending index of the search range, exclusive
+ * @return an <code>StringMatcher.Position</code> object that keeps the starting
+ * (inclusive) and ending positions (exclusive) of the first occurrence of the
+ * pattern in the specified range of the text; return null if not found or subtext
+ * is empty (start==end). A pair of zeros is returned if pattern is empty string
+ * Note that for pattern like "*abc*" with leading and trailing stars, position of "abc"
+ * is returned. For a pattern like"*??*" in text "abcdf", (1,3) is returned
+ */
+
+ public StringMatcher.Position find(String text, int start, int end) {
+ if (fPattern == null|| text == null)
+ throw new IllegalArgumentException();
+
+ int tlen = text.length();
+ if (start < 0)
+ start = 0;
+ if (end > tlen)
+ end = tlen;
+ if (end < 0 ||start >= end )
+ return null;
+ if (fLength == 0)
+ return new Position(start, start);
+ if (fIgnoreWildCards) {
+ int x = posIn(text, start, end);
+ if (x < 0)
+ return null;
+ return new Position(x, x+fLength);
+ }
+
+ int segCount = fSegments.length;
+ if (segCount == 0)//pattern contains only '*'(s)
+ return new Position (start, end);
+
+ int curPos = start;
+ int matchStart = -1;
+ int i;
+ for (i = 0; i < segCount && curPos < end; ++i) {
+ String current = fSegments[i];
+ int nextMatch = regExpPosIn(text, curPos, end, current);
+ if (nextMatch < 0 )
+ return null;
+ if(i == 0)
+ matchStart = nextMatch;
+ curPos = nextMatch + current.length();
+ }
+ if (i < segCount)
+ return null;
+ return new Position(matchStart, curPos);
+ }
+
+ /**
+ * match the given <code>text</code> with the pattern
+ * @return <code>true</code> if matched otherwise <code>false</code>
+ * @param text a String object
+ */
+ public boolean match(String text) {
+ return match(text, 0, text.length());
+ }
+ /**
+ * Given the starting (inclusive) and the ending (exclusive) positions in the
+ * <code>text</code>, determine if the given substring matches with aPattern
+ * @return true if the specified portion of the text matches the pattern
+ * @param text a String object that contains the substring to match
+ * @param start marks the starting position (inclusive) of the substring
+ * @param end marks the ending index (exclusive) of the substring
+ */
+ public boolean match(String text, int start, int end) {
+ if (null == text)
+ throw new IllegalArgumentException();
+
+ if (start > end)
+ return false;
+
+ if (fIgnoreWildCards)
+ return (end - start == fLength) && fPattern.regionMatches(fIgnoreCase, 0, text, start, fLength);
+ int segCount= fSegments.length;
+ if (segCount == 0 && (fHasLeadingStar || fHasTrailingStar)) // pattern contains only '*'(s)
+ return true;
+ if (start == end)
+ return fLength == 0;
+ if (fLength == 0)
+ return start == end;
+
+ int tlen= text.length();
+ if (start < 0)
+ start= 0;
+ if (end > tlen)
+ end= tlen;
+
+ int tCurPos= start;
+ int bound= end - fBound;
+ if ( bound < 0)
+ return false;
+ int i=0;
+ String current= fSegments[i];
+ int segLength= current.length();
+
+ /* process first segment */
+ if (!fHasLeadingStar){
+ if(!regExpRegionMatches(text, start, current, 0, segLength)) {
+ return false;
+ } else {
+ ++i;
+ tCurPos= tCurPos + segLength;
+ }
+ }
+ if ((fSegments.length == 1) && (!fHasLeadingStar) && (!fHasTrailingStar)) {
+ // only one segment to match, no wildcards specified
+ return tCurPos == end;
+ }
+ /* process middle segments */
+ while (i < segCount) {
+ current= fSegments[i];
+ int currentMatch;
+ int k= current.indexOf(fSingleWildCard);
+ if (k < 0) {
+ currentMatch= textPosIn(text, tCurPos, end, current);
+ if (currentMatch < 0)
+ return false;
+ } else {
+ currentMatch= regExpPosIn(text, tCurPos, end, current);
+ if (currentMatch < 0)
+ return false;
+ }
+ tCurPos= currentMatch + current.length();
+ i++;
+ }
+
+ /* process final segment */
+ if (!fHasTrailingStar && tCurPos != end) {
+ int clen= current.length();
+ return regExpRegionMatches(text, end - clen, current, 0, clen);
+ }
+ return i == segCount ;
+ }
+
+ /**
+ * check existence of '/' in the pattern.
+ * @return <b>true</b> if pattern contains '/'
+ */
+ public boolean isPathPattern() {
+ return fPathPattern;
+ }
+
+ /**
+ * This method parses the given pattern into segments seperated by wildcard '*' characters.
+ * Since wildcards are not being used in this case, the pattern consists of a single segment.
+ */
+ private void parseNoWildCards() {
+ fSegments = new String[1];
+ fSegments[0] = fPattern;
+ fBound = fLength;
+ }
+ /**
+ * Parses the given pattern into segments seperated by wildcard '*' characters.
+ */
+ private void parseWildCards() {
+ if(fPattern.startsWith("*"))//$NON-NLS-1$
+ fHasLeadingStar = true;
+ if(fPattern.endsWith("*")) {//$NON-NLS-1$
+ /* make sure it's not an escaped wildcard */
+ if (fLength > 1 && fPattern.charAt(fLength - 2) != '\\') {
+ fHasTrailingStar = true;
+ }
+ }
+
+ Vector<String> temp = new Vector<>();
+
+ int pos = 0;
+ StringBuilder buf = new StringBuilder();
+ while (pos < fLength) {
+ char c = fPattern.charAt(pos++);
+ switch (c) {
+ case '\\':
+ if (pos >= fLength) {
+ buf.append(c);
+ } else {
+ char next = fPattern.charAt(pos++);
+ /* if it's an escape sequence */
+ if (next == '*' || next == '?' || next == '\\') {
+ buf.append(next);
+ } else {
+ /* not an escape sequence, just insert literally */
+ buf.append(c);
+ buf.append(next);
+ }
+ }
+ break;
+ case '*':
+ if (buf.length() > 0) {
+ /* new segment */
+ temp.addElement(buf.toString());
+ fBound += buf.length();
+ buf.setLength(0);
+ }
+ break;
+ case '?':
+ /* append special character representing single match wildcard */
+ buf.append(fSingleWildCard);
+ break;
+ default:
+ buf.append(c);
+ }
+ }
+
+ /* add last buffer to segment list */
+ if (buf.length() > 0) {
+ temp.addElement(buf.toString());
+ fBound += buf.length();
+ }
+
+ fSegments = new String[temp.size()];
+ temp.copyInto(fSegments);
+ }
+ /**
+ * @param text a string which contains no wildcard
+ * @param start the starting index in the text for search, inclusive
+ * @param end the stopping point of search, exclusive
+ * @return the starting index in the text of the pattern , or -1 if not found
+ */
+ protected int posIn(String text, int start, int end) {//no wild card in pattern
+ int max = end - fLength;
+
+ if (!fIgnoreCase) {
+ int i = text.indexOf(fPattern, start);
+ if (i == -1 || i > max)
+ return -1;
+ return i;
+ }
+
+ for (int i = start; i <= max; ++i) {
+ if (text.regionMatches(true, i, fPattern, 0, fLength))
+ return i;
+ }
+
+ return -1;
+ }
+ /**
+ * @param text a simple regular expression that may only contain '?'(s)
+ * @param start the starting index in the text for search, inclusive
+ * @param end the stopping point of search, exclusive
+ * @param p a simple regular expression that may contains '?'
+ * @return the starting index in the text of the pattern , or -1 if not found
+ */
+ protected int regExpPosIn(String text, int start, int end, String p) {
+ int plen = p.length();
+
+ int max = end - plen;
+ for (int i = start; i <= max; ++i) {
+ if (regExpRegionMatches(text, i, p, 0, plen))
+ return i;
+ }
+ return -1;
+ }
+
+ /**
+ *
+ * @param text the text
+ * @param tStart the start
+ * @param p the pattern
+ * @param pStart the pattern start
+ * @param plen the pattern length
+ * @return whether the region matches
+ */
+ protected boolean regExpRegionMatches(String text, int tStart, String p, int pStart, int plen) {
+ while (plen-- > 0) {
+ char tchar = text.charAt(tStart++);
+ char pchar = p.charAt(pStart++);
+
+ /* process wild cards */
+ if (!fIgnoreWildCards) {
+ /* skip single wild cards */
+ if (pchar == fSingleWildCard) {
+ continue;
+ }
+ }
+ if (pchar == tchar)
+ continue;
+ if (fIgnoreCase) {
+ if (Character.toUpperCase(tchar) == Character.toUpperCase(pchar))
+ continue;
+ // comparing after converting to upper case doesn't handle all cases;
+ // also compare after converting to lower case
+ if (Character.toLowerCase(tchar) == Character.toLowerCase(pchar))
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+ /**
+ * @param text the string to match
+ * @param start the starting index in the text for search, inclusive
+ * @param end the stopping point of search, exclusive
+ * @param p a string that has no wildcard
+ * @return the starting index in the text of the pattern , or -1 if not found
+ */
+ protected int textPosIn(String text, int start, int end, String p) {
+
+ int plen = p.length();
+ int max = end - plen;
+
+ if (!fIgnoreCase) {
+ int i = text.indexOf(p, start);
+ if (i == -1 || i > max)
+ return -1;
+ return i;
+ }
+
+ for (int i = start; i <= max; ++i) {
+ if (text.regionMatches(true, i, p, 0, plen))
+ return i;
+ }
+
+ return -1;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamHookDispatcher.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamHookDispatcher.java
new file mode 100644
index 000000000..96c8aae58
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamHookDispatcher.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.net.URI;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.resources.team.ResourceRuleFactory;
+import org.eclipse.core.resources.team.TeamHook;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.team.core.RepositoryProvider;
+
+/**
+ * This class forwards TeamHook callbacks to the proper RepositoryProvider
+ */
+public class TeamHookDispatcher extends TeamHook {
+
+ private static final ResourceRuleFactory defaultFactory = new ResourceRuleFactory() {};
+ private static TeamHookDispatcher instance;
+
+ public static void setProviderRuleFactory(IProject project, IResourceRuleFactory factory) {
+ if (instance != null) {
+ if (factory == null) {
+ factory = defaultFactory;
+ }
+ instance.setRuleFactory(project, factory);
+ }
+ }
+
+ public TeamHookDispatcher() {
+ instance = this;
+ }
+
+ @Override
+ public IStatus validateCreateLink(IFile file, int updateFlags, IPath location) {
+ RepositoryProvider provider = getProvider(file);
+ if (provider == null) {
+ return super.validateCreateLink(file, updateFlags, location);
+ } else {
+ return provider.validateCreateLink(file, updateFlags, location);
+ }
+ }
+
+ @Override
+ public IStatus validateCreateLink(IFile file, int updateFlags, URI location) {
+ RepositoryProvider provider = getProvider(file);
+ if (provider == null) {
+ return super.validateCreateLink(file, updateFlags, location);
+ } else {
+ return provider.validateCreateLink(file, updateFlags, location);
+ }
+ }
+
+ @Override
+ public IStatus validateCreateLink(IFolder folder, int updateFlags, IPath location) {
+ RepositoryProvider provider = getProvider(folder);
+ if (provider == null) {
+ return super.validateCreateLink(folder, updateFlags, location);
+ } else {
+ return provider.validateCreateLink(folder, updateFlags, location);
+ }
+ }
+
+ @Override
+ public IStatus validateCreateLink(IFolder folder, int updateFlags, URI location) {
+ RepositoryProvider provider = getProvider(folder);
+ if (provider == null) {
+ return super.validateCreateLink(folder, updateFlags, location);
+ } else {
+ return provider.validateCreateLink(folder, updateFlags, location);
+ }
+ }
+
+ /**
+ * Method getProvider.
+ * @param folder
+ * @return RepositoryProvider
+ */
+ private RepositoryProvider getProvider(IResource resource) {
+ return RepositoryProvider.getProvider(resource.getProject());
+ }
+
+ @Override
+ public IResourceRuleFactory getRuleFactory(IProject project) {
+ if (RepositoryProvider.isShared(project)) {
+ RepositoryProvider provider = getProvider(project);
+ // Provider can be null if the provider plugin is not available
+ if (provider != null) {
+ return provider.getRuleFactory();
+ }
+ }
+ // Use the default provided by the superclass
+ return super.getRuleFactory(project);
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamPlugin.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamPlugin.java
new file mode 100644
index 000000000..3d45864f5
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamPlugin.java
@@ -0,0 +1,225 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.io.*;
+import java.util.*;
+
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.content.IContentDescription;
+import org.eclipse.core.runtime.content.IContentTypeManager;
+import org.eclipse.osgi.service.debug.DebugOptions;
+import org.eclipse.osgi.service.debug.DebugOptionsListener;
+import org.eclipse.team.core.*;
+import org.eclipse.team.core.mapping.DelegatingStorageMerger;
+import org.eclipse.team.internal.core.mapping.IStreamMergerDelegate;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * <code>TeamPlugin</code> is the plug-in runtime class for the Team
+ * resource management plugin.
+ * <p>
+ *
+ * @see Team
+ * @see RepositoryProvider
+ *
+ * @since 2.0
+ */
+final public class TeamPlugin extends Plugin {
+
+ // The id of the core team plug-in
+ public static final String ID = "org.eclipse.team.core"; //$NON-NLS-1$
+
+ // The id of the providers extension point
+ public static final String PROVIDER_EXTENSION = "repository-provider-type"; //$NON-NLS-1$
+
+ // The id of the file types extension point
+ public static final String FILE_TYPES_EXTENSION = "fileTypes"; //$NON-NLS-1$
+
+ // The id of the global ignore extension point
+ public static final String IGNORE_EXTENSION = "ignore"; //$NON-NLS-1$
+ // The id of the project set extension point
+ public static final String PROJECT_SET_EXTENSION = "projectSets"; //$NON-NLS-1$
+ // The id of the repository extension point
+ public static final String REPOSITORY_EXTENSION = "repository"; //$NON-NLS-1$
+ // The id of the default file modification validator extension point
+ public static final String DEFAULT_FILE_MODIFICATION_VALIDATOR_EXTENSION = "defaultFileModificationValidator"; //$NON-NLS-1$
+
+ // The id used to associate a provider with a project
+ public final static QualifiedName PROVIDER_PROP_KEY =
+ new QualifiedName("org.eclipse.team.core", "repository"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // The id for the Bundle Import extension point
+ public static final String EXTENSION_POINT_BUNDLE_IMPORTERS = ID + ".bundleImporters"; //$NON-NLS-1$
+
+ // The one and only plug-in instance
+ private static TeamPlugin plugin;
+
+ private ServiceRegistration debugRegistration;
+ private IStreamMergerDelegate mergerDelegate;
+
+ /**
+ * Constructs a plug-in runtime class.
+ */
+ public TeamPlugin() {
+ super();
+ plugin = this;
+ }
+
+ @Override
+ public void start(BundleContext context) throws Exception {
+ super.start(context);
+
+ // register debug options listener
+ Hashtable<String, String> properties = new Hashtable<>(2);
+ properties.put(DebugOptions.LISTENER_SYMBOLICNAME, ID);
+ debugRegistration = context.registerService(DebugOptionsListener.class, Policy.DEBUG_OPTIONS_LISTENER, properties);
+
+ Team.startup();
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ try {
+ // unregister debug options listener
+ debugRegistration.unregister();
+ debugRegistration = null;
+
+ Team.shutdown();
+ ResourceVariantCache.shutdown();
+ } finally {
+ super.stop(context);
+ }
+ }
+
+ /**
+ * Returns the Team plug-in.
+ *
+ * @return the single instance of this plug-in runtime class
+ */
+ public static TeamPlugin getPlugin() {
+ return plugin;
+ }
+
+ /**
+ * Log the given exception allowing with the provided message and severity indicator
+ * @param severity the severity
+ * @param message the message
+ * @param e the exception
+ */
+ public static void log(int severity, String message, Throwable e) {
+ plugin.getLog().log(new Status(severity, ID, 0, message, e));
+ }
+
+ /**
+ * Log the given CoreException in a manner that will include the stacktrace of
+ * the exception in the log.
+ * @param e the exception
+ */
+ public static void log(CoreException e) {
+ log(e.getStatus().getSeverity(), e.getMessage(), e);
+ }
+
+ /*
+ * Static helper methods for creating exceptions
+ */
+ public static TeamException wrapException(CoreException e) {
+ IStatus status = e.getStatus();
+ return new TeamException(new Status(status.getSeverity(), ID, status.getCode(), status.getMessage(), e));
+ }
+
+ public static String getCharset(String name, InputStream stream) throws IOException {
+ IContentDescription description = getContentDescription(name, stream);
+ return description == null ? null : description.getCharset();
+
+ }
+ public static IContentDescription getContentDescription(String name, InputStream stream) throws IOException {
+ // tries to obtain a description for this file contents
+ IContentTypeManager contentTypeManager = Platform.getContentTypeManager();
+ try {
+ return contentTypeManager.getDescriptionFor(stream, name, IContentDescription.ALL);
+ } finally {
+ if (stream != null)
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // Ignore exceptions on close
+ }
+ }
+ }
+
+ public static RepositoryProviderType getAliasType(String id) {
+ IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint(TeamPlugin.ID, TeamPlugin.REPOSITORY_EXTENSION);
+ if (extension != null) {
+ IExtension[] extensions = extension.getExtensions();
+ for (int i = 0; i < extensions.length; i++) {
+ IConfigurationElement [] configElements = extensions[i].getConfigurationElements();
+ for (int j = 0; j < configElements.length; j++) {
+ String aliasId = configElements[j].getAttribute("canImportId"); //$NON-NLS-1$
+ if (aliasId != null && aliasId.equals(id)) {
+ String extensionId = configElements[j].getAttribute("id"); //$NON-NLS-1$
+ if (extensionId != null) {
+ return RepositoryProviderType.getProviderType(extensionId);
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public static IPath[] getMetaFilePaths(String id) {
+ IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint(TeamPlugin.ID, TeamPlugin.REPOSITORY_EXTENSION);
+ if (extension != null) {
+ IExtension[] extensions = extension.getExtensions();
+ for (int i = 0; i < extensions.length; i++) {
+ IConfigurationElement [] configElements = extensions[i].getConfigurationElements();
+ for (int j = 0; j < configElements.length; j++) {
+ String extensionId = configElements[j].getAttribute("id"); //$NON-NLS-1$
+ String metaFilePaths = configElements[j].getAttribute("metaFilePaths"); //$NON-NLS-1$
+ if (extensionId != null && extensionId.equals(id) && metaFilePaths != null) {
+ return getPaths(metaFilePaths);
+
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static IPath[] getPaths(String metaFilePaths) {
+ List<IPath> result = new ArrayList<>();
+ StringTokenizer t = new StringTokenizer(metaFilePaths, ","); //$NON-NLS-1$
+ while (t.hasMoreTokens()) {
+ String next = t.nextToken();
+ IPath path = new Path(null, next);
+ result.add(path);
+ }
+ return result.toArray(new IPath[result.size()]);
+ }
+
+ /**
+ * Set the file merger that is used by the {@link DelegatingStorageMerger#merge(OutputStream, String, IStorage, IStorage, IStorage, IProgressMonitor)}
+ * method. It is the responsibility of subclasses to provide a merger.
+ * If a merger is not provided, subclasses must override <code>performThreeWayMerge</code>.
+ * @param merger the merger used to merge files
+ */
+ public void setMergerDelegate(IStreamMergerDelegate merger) {
+ mergerDelegate = merger;
+ }
+
+ public IStreamMergerDelegate getMergerDelegate() {
+ return mergerDelegate;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamResourceChangeListener.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamResourceChangeListener.java
new file mode 100644
index 000000000..613189dc4
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/TeamResourceChangeListener.java
@@ -0,0 +1,156 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 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.internal.core;
+
+import java.util.*;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.RepositoryProvider;
+import org.eclipse.team.core.RepositoryProviderType;
+
+/**
+ * Change listener that detects and handles project moves and
+ * meta-file creation.
+ */
+public final class TeamResourceChangeListener implements IResourceChangeListener {
+
+ private static final Map<String, IPath[]> metaFilePaths; // Map of String (repository id) -> IPath[]
+
+ static {
+ metaFilePaths = new HashMap<>();
+ String[] ids = RepositoryProvider.getAllProviderTypeIds();
+ for (int i = 0; i < ids.length; i++) {
+ String id = ids[i];
+ IPath[] paths = TeamPlugin.getMetaFilePaths(id);
+ if (paths != null) {
+ metaFilePaths.put(id, paths);
+ }
+ }
+ }
+
+ @Override
+ public void resourceChanged(IResourceChangeEvent event) {
+ IResourceDelta[] projectDeltas = event.getDelta().getAffectedChildren();
+ for (int i = 0; i < projectDeltas.length; i++) {
+ IResourceDelta delta = projectDeltas[i];
+ IResource resource = delta.getResource();
+ IProject project = resource.getProject();
+ if (!RepositoryProvider.isShared(project)) {
+ // Look for meta-file creation in unshared projects
+ handleUnsharedProjectChanges(project, delta);
+ } else {
+ // Handle project moves
+ // Only consider project additions that are moves
+ if (delta.getKind() != IResourceDelta.ADDED) continue;
+ if ((delta.getFlags() & IResourceDelta.MOVED_FROM) == 0) continue;
+ // Only consider projects that have a provider
+ RepositoryProvider provider = RepositoryProvider.getProvider(project);
+ if (provider == null) continue;
+ // Only consider providers whose project is not mapped properly already
+ if (provider.getProject().equals(project)) continue;
+ // Tell the provider about it's new project
+ provider.setProject(project);
+ }
+ }
+ }
+
+ private void handleUnsharedProjectChanges(IProject project, IResourceDelta delta) {
+ String repositoryId = null;
+ Set<IContainer> metaFileContainers = new HashSet<>();
+ Set<String> badIds = new HashSet<>();
+ IFile[] files = getAddedFiles(delta);
+ for (int i = 0; i < files.length; i++) {
+ IFile file = files[i];
+ String typeId = getMetaFileType(file);
+ if (typeId != null) {
+ // The file path matches the path for the given type id
+ if (repositoryId == null) {
+ repositoryId = typeId;
+ } else if (!repositoryId.equals(typeId) && !badIds.contains(typeId)) {
+ TeamPlugin.log(IStatus.WARNING, "Meta files for two repository types (" + repositoryId + " and " + typeId + " was found in project " + project.getName() + ".", null); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+ badIds.add(typeId);
+ }
+ if (typeId.equals(repositoryId)) {
+ IContainer container = getContainer(typeId, file);
+ metaFileContainers.add(container);
+ }
+ }
+ }
+ if (repositoryId != null) {
+ RepositoryProviderType type = RepositoryProviderType.getProviderType(repositoryId);
+ type.metaFilesDetected(project, metaFileContainers.toArray(new IContainer[metaFileContainers.size()]));
+ }
+ }
+
+ private IContainer getContainer(String typeId, IFile file) {
+ IPath[] paths = metaFilePaths.get(typeId);
+ IPath foundPath = null;
+ IPath projectRelativePath = file.getProjectRelativePath();
+ for (int i = 0; i < paths.length; i++) {
+ IPath path = paths[i];
+ if (isSuffix(projectRelativePath, path)) {
+ foundPath = path;
+ }
+ }
+ IResource resource = file;
+ if (foundPath != null) {
+ for (int i = 0; i < foundPath.segmentCount(); i++) {
+ resource = resource.getParent();
+ }
+ }
+ if (resource.getType() == IResource.FILE) {
+ return file.getParent();
+ }
+ return (IContainer)resource;
+ }
+
+ private String getMetaFileType(IFile file) {
+ for (Iterator<String> iter = metaFilePaths.keySet().iterator(); iter.hasNext();) {
+ String id = iter.next();
+ IPath[] paths = metaFilePaths.get(id);
+ for (int i = 0; i < paths.length; i++) {
+ IPath path = paths[i];
+ if (isSuffix(file.getProjectRelativePath(), path)) {
+ return id;
+ }
+ }
+ }
+ return null;
+ }
+
+ private boolean isSuffix(IPath path, IPath suffix) {
+ if (path.segmentCount() < suffix.segmentCount())
+ return false;
+ for (int i = 0; i < suffix.segmentCount(); i++) {
+ if (!suffix.segment(i).equals(path.segment(path.segmentCount() - suffix.segmentCount() + i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private IFile[] getAddedFiles(IResourceDelta delta) {
+ final List<IFile> result = new ArrayList<>();
+ try {
+ delta.accept(delta1 -> {
+ if ((delta1.getKind() & IResourceDelta.ADDED) != 0
+ && delta1.getResource().getType() == IResource.FILE) {
+ result.add((IFile) delta1.getResource());
+ }
+ return true;
+ });
+ } catch (CoreException e) {
+ TeamPlugin.log(IStatus.ERROR, "An error occurred while scanning for meta-file changes", e); //$NON-NLS-1$
+ }
+ return result.toArray(new IFile[result.size()]);
+ }
+} \ No newline at end of file
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/UserStringMappings.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/UserStringMappings.java
new file mode 100644
index 000000000..256f2bad6
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/UserStringMappings.java
@@ -0,0 +1,116 @@
+/*******************************************************************************
+ * 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.internal.core;
+
+import java.util.*;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.Preferences;
+import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
+import org.eclipse.team.core.Team;
+
+
+public class UserStringMappings implements Preferences.IPropertyChangeListener {
+
+ public static final Integer BINARY= Integer.valueOf(Team.BINARY);
+ public static final Integer TEXT= Integer.valueOf(Team.TEXT);
+ public static final Integer UNKNOWN= Integer.valueOf(Team.UNKNOWN);
+
+
+ private static final String PREF_TEAM_SEPARATOR = "\n"; //$NON-NLS-1$
+
+ private final Preferences fPreferences;
+ private final String fKey;
+
+ private Map<String, Integer> fMap;
+
+ public UserStringMappings(String key) {
+ fKey= key;
+ fPreferences= TeamPlugin.getPlugin().getPluginPreferences();
+ fPreferences.addPropertyChangeListener(this);
+ }
+
+ public Map<String, Integer> referenceMap() {
+ if (fMap == null) {
+ fMap= loadMappingsFromPreferences();
+ }
+ return fMap;
+ }
+
+ public void addStringMappings(String[] names, int[] types) {
+ Assert.isTrue(names.length == types.length);
+ final Map<String, Integer> map= referenceMap();
+
+ for (int i = 0; i < names.length; i++) {
+ switch (types[i]) {
+ case Team.BINARY: map.put(names[i], BINARY); break;
+ case Team.TEXT: map.put(names[i], TEXT); break;
+ case Team.UNKNOWN: map.put(names[i], UNKNOWN); break;
+ }
+ }
+ save();
+ }
+
+ public void setStringMappings(String [] names, int [] types) {
+ Assert.isTrue(names.length == types.length);
+ referenceMap().clear();
+ addStringMappings(names, types);
+ }
+
+ public int getType(String string) {
+ if (string == null)
+ return Team.UNKNOWN;
+ final Integer type= referenceMap().get(string);
+ return type != null ? type.intValue() : Team.UNKNOWN;
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent event) {
+ if(event.getProperty().equals(fKey))
+ fMap= null;
+ }
+
+ public void save() {
+ // Now set into preferences
+ final StringBuilder buffer = new StringBuilder();
+ final Iterator e = fMap.keySet().iterator();
+
+ while (e.hasNext()) {
+ final String filename = (String)e.next();
+ buffer.append(filename);
+ buffer.append(PREF_TEAM_SEPARATOR);
+ final Integer type = fMap.get(filename);
+ buffer.append(type);
+ buffer.append(PREF_TEAM_SEPARATOR);
+ }
+ TeamPlugin.getPlugin().getPluginPreferences().setValue(fKey, buffer.toString());
+ }
+
+ protected Map<String, Integer> loadMappingsFromPreferences() {
+ final Map<String, Integer> result= new HashMap<>();
+
+ if (!fPreferences.contains(fKey))
+ return result;
+
+ final String prefTypes = fPreferences.getString(fKey);
+ final StringTokenizer tok = new StringTokenizer(prefTypes, PREF_TEAM_SEPARATOR);
+ try {
+ while (tok.hasMoreElements()) {
+ final String name = tok.nextToken();
+ final String mode= tok.nextToken();
+ result.put(name, Integer.valueOf(mode));
+ }
+ } catch (NoSuchElementException e) {
+ }
+ return result;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/history/LocalFileHistory.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/history/LocalFileHistory.java
new file mode 100644
index 000000000..0c6c80944
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/history/LocalFileHistory.java
@@ -0,0 +1,128 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.history;
+
+import java.util.ArrayList;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFileState;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.history.IFileRevision;
+import org.eclipse.team.core.history.provider.FileHistory;
+import org.eclipse.team.internal.core.Messages;
+
+public class LocalFileHistory extends FileHistory {
+
+ protected IFile file;
+ //used to hold all revisions (changes based on filtering)
+ protected IFileRevision[] revisions;
+ private final boolean includeCurrent;
+
+ /*
+ * Creates a new CVSFile history that will fetch remote revisions by default.
+ */
+ public LocalFileHistory(IFile file, boolean includeCurrent) {
+ this.file = file;
+ this.includeCurrent = includeCurrent;
+ }
+
+ @Override
+ public IFileRevision[] getContributors(IFileRevision revision) {
+
+ IFileRevision[] revisions = getFileRevisions();
+
+ //the predecessor is the file with a timestamp that is the largest timestamp
+ //from the set of all timestamps smaller than the root file's timestamp
+ IFileRevision fileRevision = null;
+ for (int i = 0; i < revisions.length; i++) {
+ if (((LocalFileRevision) revisions[i]).isPredecessorOf(revision)) {
+ //no revision has been set as of yet
+ if (fileRevision == null)
+ fileRevision = revisions[i];
+ //this revision is a predecessor - now check to see if it comes
+ //after the current predecessor, if it does make it the current predecessor
+ if (fileRevision != null && revisions[i].getTimestamp() > fileRevision.getTimestamp()) {
+ fileRevision = revisions[i];
+ }
+ }
+ }
+ if (fileRevision == null)
+ return new IFileRevision[0];
+ return new IFileRevision[] {fileRevision};
+ }
+
+ @Override
+ public IFileRevision getFileRevision(String id) {
+ if (revisions != null) {
+ for (int i = 0; i < revisions.length; i++) {
+ IFileRevision revision = revisions[i];
+ if (revision.getContentIdentifier().equals(id)) {
+ return revision;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public IFileRevision[] getFileRevisions() {
+ if (revisions == null)
+ return new IFileRevision[0];
+ return revisions;
+ }
+
+ @Override
+ public IFileRevision[] getTargets(IFileRevision revision) {
+ IFileRevision[] revisions = getFileRevisions();
+ ArrayList<IFileRevision> directDescendents = new ArrayList<>();
+
+ for (int i = 0; i < revisions.length; i++) {
+ if (((LocalFileRevision) revisions[i]).isDescendentOf(revision)) {
+ directDescendents.add(revisions[i]);
+ }
+ }
+ return directDescendents.toArray(new IFileRevision[directDescendents.size()]);
+ }
+
+ /**
+ * Refreshes the revisions for this local file.
+ *
+ * @param monitor a progress monitor
+ * @throws TeamException
+ */
+ public void refresh(IProgressMonitor monitor) throws TeamException {
+ monitor.beginTask(Messages.LocalFileHistory_RefreshLocalHistory/*, file.getProjectRelativePath().toString())*/, 300);
+ try {
+ // Include the file's current state if and only if the file exists.
+ LocalFileRevision currentRevision =
+ (includeRevisionForFile() ? new LocalFileRevision(file) : null);
+ IFileState[] fileStates = file.getHistory(monitor);
+ int numRevisions = fileStates.length + (currentRevision != null ? 1 : 0);
+ revisions = new LocalFileRevision[numRevisions];
+ for (int i = 0; i < fileStates.length; i++) {
+ revisions[i] = new LocalFileRevision(fileStates[i]);
+ }
+ if (currentRevision != null)
+ revisions[fileStates.length] = currentRevision;
+ } catch (CoreException e) {
+ throw TeamException.asTeamException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ private boolean includeRevisionForFile() {
+ return file.exists() && includeCurrent;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/history/LocalFileRevision.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/history/LocalFileRevision.java
new file mode 100644
index 000000000..1274e0c65
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/history/LocalFileRevision.java
@@ -0,0 +1,204 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.history;
+
+import java.net.URI;
+
+import org.eclipse.core.filesystem.URIUtil;
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.history.IFileRevision;
+import org.eclipse.team.core.history.ITag;
+import org.eclipse.team.core.history.provider.FileRevision;
+import org.eclipse.team.internal.core.Messages;
+
+/**
+ * A LocalFileRevision is used for wrapping local files in order to display
+ * them in the History View. As such, this class can be used to wrap either
+ * a local file's IFileState or an IFile.
+ */
+public class LocalFileRevision extends FileRevision {
+ /*
+ * Either one or the other of these fields is intended
+ * to be used.
+ */
+ //Used for wrapping local file history items
+ private IFileState state;
+
+ //Used for displaying the "real" current file
+ private IFile file;
+ //Used for displaying which base revision
+ private IFileRevision baseRevision;
+
+ /*
+ * Used for wrapping an IFileState.
+ */
+ public LocalFileRevision(IFileState state) {
+ this.state = state;
+ this.file = null;
+ this.baseRevision = null;
+ }
+
+ /*
+ * Used for wrapping an IFile. This is generally used to represent the local
+ * current version of the file being displayed in the history. Make sure to
+ * also pass in the base revision associated with this current version.
+ *
+ * @see #setBaseRevision(IFileRevision)
+ */
+ public LocalFileRevision(IFile file) {
+ this.file = file;
+ this.baseRevision = null;
+ this.state = null;
+ }
+
+ @Override
+ public String getContentIdentifier() {
+ if (file != null)
+ return baseRevision == null ? NLS.bind(Messages.LocalFileRevision_currentVersion, "") : NLS.bind(Messages.LocalFileRevision_currentVersion, baseRevision.getContentIdentifier()); //$NON-NLS-1$
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getAuthor() {
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getComment() {
+ if (file != null)
+ return Messages.LocalFileRevision_currentVersionTag;
+ return null;
+ }
+
+ @Override
+ public ITag[] getTags() {
+ return new ITag[0];
+ }
+
+ @Override
+ public IStorage getStorage(IProgressMonitor monitor) throws CoreException {
+ if (file != null) {
+ return file;
+ }
+ return state;
+ }
+
+ @Override
+ public String getName() {
+ if (file != null) {
+ return file.getName();
+ }
+
+ return state.getName();
+ }
+
+ @Override
+ public long getTimestamp() {
+ if (file != null) {
+ return file.getLocalTimeStamp();
+ }
+
+ return state.getModificationTime();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.team.core.history.provider.FileRevision#exists()
+ * A LocalFileRevision generally should exist, but if it doesn't, this
+ * method should tell the truth.
+ */
+ @Override
+ public boolean exists() {
+ if (file != null) {
+ return file.exists();
+ }
+
+ return state.exists();
+ }
+
+ /*
+ * Sets the base revision. Can be used to associate a base revision
+ * with an IFile.
+ */
+ public void setBaseRevision(IFileRevision baseRevision) {
+ this.baseRevision = baseRevision;
+ }
+
+ @Override
+ public boolean isPropertyMissing() {
+ return true;
+ }
+
+
+ @Override
+ public IFileRevision withAllProperties(IProgressMonitor monitor) {
+ return this;
+ }
+
+ public boolean isPredecessorOf(IFileRevision revision) {
+ long compareRevisionTime = revision.getTimestamp();
+ return (this.getTimestamp() < compareRevisionTime);
+ }
+
+ public boolean isDescendentOf(IFileRevision revision) {
+ long compareRevisionTime = revision.getTimestamp();
+ return (this.getTimestamp() > compareRevisionTime);
+ }
+
+ @Override
+ public URI getURI() {
+ if (file != null)
+ return file.getLocationURI();
+
+ return URIUtil.toURI(state.getFullPath());
+ }
+
+ public IFile getFile() {
+ return file;
+ }
+
+ public IFileState getState() {
+ return state;
+ }
+
+ public boolean isCurrentState() {
+ return file != null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this)
+ return true;
+ if (obj instanceof LocalFileRevision) {
+ LocalFileRevision other = (LocalFileRevision) obj;
+ if (file != null && other.file != null)
+ return file.equals(other.file);
+ if (state != null && other.state != null)
+ return statesEqual(state, other.state);
+ }
+ return false;
+ }
+
+ private boolean statesEqual(IFileState s1, IFileState s2) {
+ return (s1.getFullPath().equals(s2.getFullPath()) && s1.getModificationTime() == s2.getModificationTime());
+ }
+
+ @Override
+ public int hashCode() {
+ if (file != null)
+ return file.hashCode();
+ if (state != null)
+ return (int)state.getModificationTime();
+ return super.hashCode();
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/importing/BundleImporterExtension.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/importing/BundleImporterExtension.java
new file mode 100644
index 000000000..196fe5039
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/importing/BundleImporterExtension.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright (c) 2011, 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.internal.core.importing;
+
+import java.util.*;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.RepositoryProviderType;
+import org.eclipse.team.core.ScmUrlImportDescription;
+import org.eclipse.team.core.importing.provisional.*;
+import org.eclipse.team.internal.core.TeamPlugin;
+
+/**
+ * A bundle importer extension.
+ *
+ * @since 3.7
+ */
+public class BundleImporterExtension implements IBundleImporter {
+
+ private IBundleImporterDelegate delegate;
+ private IConfigurationElement element;
+
+ /**
+ * Constructs a bundle importer extension on the given element.
+ *
+ * @param element contribution
+ */
+ public BundleImporterExtension(IConfigurationElement element) {
+ this.element = element;
+ }
+
+ @Override
+ public ScmUrlImportDescription[] validateImport(Map[] manifests) {
+ try {
+ return getDelegate().validateImport(manifests);
+ } catch (CoreException e) {
+ TeamPlugin.log(e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns underlying delegate.
+ *
+ * @return delegate
+ * @exception CoreException if unable to instantiate delegate
+ */
+ private synchronized IBundleImporterDelegate getDelegate() throws CoreException {
+ if (delegate == null) {
+ delegate = new BundleImporterDelegate() {
+ private Set<String> supportedValues;
+ private RepositoryProviderType providerType;
+ @Override
+ protected Set getSupportedValues() {
+ if (supportedValues == null) {
+ IConfigurationElement[] supported = element.getChildren("supports"); //$NON-NLS-1$
+ supportedValues = new HashSet<>(supported.length);
+ for (int i = 0; i < supported.length; i++) {
+ supportedValues.add(supported[i].getAttribute("prefix")); //$NON-NLS-1$
+ }
+ }
+ return supportedValues;
+ }
+ @Override
+ protected RepositoryProviderType getProviderType() {
+ if (providerType == null)
+ providerType = RepositoryProviderType.getProviderType(element.getAttribute("repository")); //$NON-NLS-1$
+ return providerType;
+ }
+ };
+ }
+ return delegate;
+ }
+
+ @Override
+ public IProject[] performImport(ScmUrlImportDescription[] descriptions, IProgressMonitor monitor) throws CoreException {
+ return getDelegate().performImport(descriptions, monitor);
+ }
+
+ @Override
+ public String getId() {
+ return element.getAttribute("id"); //$NON-NLS-1$
+ }
+
+ @Override
+ public String getDescription() {
+ return element.getAttribute("description"); //$NON-NLS-1$
+ }
+
+ @Override
+ public String getName() {
+ return element.getAttribute("name"); //$NON-NLS-1$
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/AbstractResourceMappingScope.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/AbstractResourceMappingScope.java
new file mode 100644
index 000000000..a23cdc343
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/AbstractResourceMappingScope.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * 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.internal.core.mapping;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.core.resources.mapping.*;
+import org.eclipse.team.internal.core.subscribers.AbstractSynchronizationScope;
+
+/**
+ * Class that contains common resource mapping scope code.
+ */
+public abstract class AbstractResourceMappingScope extends AbstractSynchronizationScope {
+
+ @Override
+ public ResourceMapping getMapping(Object modelObject) {
+ ResourceMapping[] mappings = getMappings();
+ for (int i = 0; i < mappings.length; i++) {
+ ResourceMapping mapping = mappings[i];
+ if (mapping.getModelObject().equals(modelObject))
+ return mapping;
+ }
+ return null;
+ }
+
+ @Override
+ public ResourceMapping[] getMappings(String id) {
+ Set<ResourceMapping> result = new HashSet<>();
+ ResourceMapping[] mappings = getMappings();
+ for (int i = 0; i < mappings.length; i++) {
+ ResourceMapping mapping = mappings[i];
+ if (mapping.getModelProviderId().equals(id)) {
+ result.add(mapping);
+ }
+ }
+ return result.toArray(new ResourceMapping[result.size()]);
+
+ }
+
+ @Override
+ public ResourceTraversal[] getTraversals(String modelProviderId) {
+ ResourceMapping[] mappings = getMappings(modelProviderId);
+ CompoundResourceTraversal traversal = new CompoundResourceTraversal();
+ for (int i = 0; i < mappings.length; i++) {
+ ResourceMapping mapping = mappings[i];
+ ResourceTraversal[] traversals = getTraversals(mapping);
+ if (traversals != null)
+ traversal.addTraversals(traversals);
+ }
+ return traversal.asTraversals();
+ }
+
+ @Override
+ public ModelProvider[] getModelProviders() {
+ Set<ModelProvider> result = new HashSet<>();
+ ResourceMapping[] mappings = getMappings();
+ for (int i = 0; i < mappings.length; i++) {
+ ResourceMapping mapping = mappings[i];
+ ModelProvider modelProvider = mapping.getModelProvider();
+ if (modelProvider != null)
+ result.add(modelProvider);
+ }
+ return result.toArray(new ModelProvider[result.size()]);
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/CompoundResourceTraversal.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/CompoundResourceTraversal.java
new file mode 100644
index 000000000..71e09aa88
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/CompoundResourceTraversal.java
@@ -0,0 +1,291 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.mapping;
+
+import java.util.*;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.mapping.ResourceTraversal;
+import org.eclipse.core.runtime.IPath;
+
+/**
+ * Helper class that accumulates several traversals in order
+ * to generate a final set of traversals and to perform certain
+ * queries on a set of traversals.
+ */
+public class CompoundResourceTraversal {
+
+ private Set<IResource> deepFolders = new HashSet<>();
+ private Set<IResource> shallowFolders = new HashSet<>();
+ private Set<IResource> zeroFolders = new HashSet<>();
+ private Set<IResource> files = new HashSet<>();
+
+ public synchronized void addTraversals(ResourceTraversal[] traversals) {
+ for (int i = 0; i < traversals.length; i++) {
+ ResourceTraversal traversal = traversals[i];
+ addTraversal(traversal);
+ }
+ }
+
+ public synchronized void addTraversal(ResourceTraversal traversal) {
+ IResource[] resources = traversal.getResources();
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ addResource(resource, traversal.getDepth());
+ }
+ }
+
+ public synchronized void addResource(IResource resource, int depth) {
+ if (resource.getType() == IResource.FILE) {
+ if (!isCovered(resource, IResource.DEPTH_ZERO))
+ files.add(resource);
+ }
+ switch (depth) {
+ case IResource.DEPTH_INFINITE:
+ addDeepFolder(resource);
+ break;
+ case IResource.DEPTH_ONE:
+ addShallowFolder(resource);
+ break;
+ case IResource.DEPTH_ZERO:
+ addZeroFolder(resource);
+ break;
+ }
+ }
+
+ private void addShallowFolder(IResource resource) {
+ if (!isCovered(resource, IResource.DEPTH_ONE)) {
+ shallowFolders.add(resource);
+ removeDescendants(resource, IResource.DEPTH_ONE);
+ }
+ }
+
+ public synchronized boolean isCovered(IResource resource, int depth) {
+ IPath fullPath = resource.getFullPath();
+ // Regardless of the depth, look for a deep folder that covers the resource
+ for (Iterator iter = deepFolders.iterator(); iter.hasNext();) {
+ IResource deepFolder = (IResource) iter.next();
+ if (deepFolder.getFullPath().isPrefixOf(fullPath)) {
+ return true;
+ }
+ }
+ // For files, look in the shallow folders and files
+ if (resource.getType() == IResource.FILE) {
+ return (shallowFolders.contains(resource.getParent()) || files.contains(resource));
+ }
+ // For folders, look in appropriate sets
+ switch (depth) {
+ case IResource.DEPTH_ONE:
+ return (shallowFolders.contains(resource));
+ case IResource.DEPTH_ZERO:
+ return (shallowFolders.contains(resource.getParent()) || zeroFolders.contains(resource));
+ }
+ return false;
+ }
+
+ private void addZeroFolder(IResource resource) {
+ if (!isCovered(resource, IResource.DEPTH_ZERO))
+ zeroFolders.add(resource);
+ }
+
+ private void addDeepFolder(IResource resource) {
+ if (!isCovered(resource, IResource.DEPTH_INFINITE)) {
+ deepFolders.add(resource);
+ removeDescendants(resource, IResource.DEPTH_INFINITE);
+ }
+ }
+
+ private void removeDescendants(IResource resource, int depth) {
+ IPath fullPath = resource.getFullPath();
+ // First, remove any files that are now covered
+ for (Iterator iter = files.iterator(); iter.hasNext();) {
+ IResource child = (IResource) iter.next();
+ switch (depth) {
+ case IResource.DEPTH_INFINITE:
+ if (fullPath.isPrefixOf(child.getFullPath())) {
+ iter.remove();
+ }
+ break;
+ case IResource.DEPTH_ONE:
+ if (fullPath.equals(child.getFullPath().removeLastSegments(1))) {
+ iter.remove();
+ }
+ break;
+ }
+ }
+ // Now, remove any shallow folders
+ if (depth == IResource.DEPTH_INFINITE) {
+ for (Iterator iter = shallowFolders.iterator(); iter.hasNext();) {
+ IResource child = (IResource) iter.next();
+ if (fullPath.isPrefixOf(child.getFullPath())) {
+ iter.remove();
+ }
+ }
+ }
+ // Finally, remove any zero folders
+ for (Iterator iter = zeroFolders.iterator(); iter.hasNext();) {
+ IResource child = (IResource) iter.next();
+ switch (depth) {
+ case IResource.DEPTH_INFINITE:
+ if (fullPath.isPrefixOf(child.getFullPath())) {
+ iter.remove();
+ }
+ break;
+ case IResource.DEPTH_ONE:
+ // TODO: Is a zero folder covered by a shallow folder?
+ if (fullPath.equals(child.getFullPath().removeLastSegments(1))) {
+ iter.remove();
+ }
+ break;
+ }
+ }
+ }
+
+ public synchronized void add(CompoundResourceTraversal compoundTraversal) {
+ // Technically, this code should synchronize on compoundTraversal.
+ // However, this makes deadlock possible and, in practive, I don't think that
+ // the provided traversal will be modified after it is passed to this method.
+ addResources(
+ compoundTraversal.deepFolders.toArray(new IResource[compoundTraversal.deepFolders.size()]),
+ IResource.DEPTH_INFINITE);
+ addResources(
+ compoundTraversal.shallowFolders.toArray(new IResource[compoundTraversal.shallowFolders.size()]),
+ IResource.DEPTH_ONE);
+ addResources(
+ compoundTraversal.zeroFolders.toArray(new IResource[compoundTraversal.zeroFolders.size()]),
+ IResource.DEPTH_ZERO);
+ addResources(
+ compoundTraversal.files.toArray(new IResource[compoundTraversal.files.size()]),
+ IResource.DEPTH_ZERO);
+ }
+
+ public synchronized void addResources(IResource[] resources, int depth) {
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ addResource(resource, depth);
+ }
+
+ }
+
+ /**
+ * Return the resources contained in the given traversals that are not covered by this traversal
+ * @param traversals the traversals being testes
+ * @return the resources contained in the given traversals that are not covered by this traversal
+ */
+ public synchronized IResource[] getUncoveredResources(ResourceTraversal[] traversals) {
+ CompoundResourceTraversal newTraversals = new CompoundResourceTraversal();
+ newTraversals.addTraversals(traversals);
+ return getUncoveredResources(newTraversals);
+ }
+
+ /*
+ * Return any resources in the other traversal that are not covered by this traversal
+ */
+ private IResource[] getUncoveredResources(CompoundResourceTraversal otherTraversal) {
+ Set<IResource> result = new HashSet<>();
+ for (Iterator iter = otherTraversal.files.iterator(); iter.hasNext();) {
+ IResource resource = (IResource) iter.next();
+ if (!isCovered(resource, IResource.DEPTH_ZERO)) {
+ result.add(resource);
+ }
+ }
+ for (Iterator iter = otherTraversal.zeroFolders.iterator(); iter.hasNext();) {
+ IResource resource = (IResource) iter.next();
+ if (!isCovered(resource, IResource.DEPTH_ZERO)) {
+ result.add(resource);
+ }
+ }
+ for (Iterator iter = otherTraversal.shallowFolders.iterator(); iter.hasNext();) {
+ IResource resource = (IResource) iter.next();
+ if (!isCovered(resource, IResource.DEPTH_ONE)) {
+ result.add(resource);
+ }
+ }
+ for (Iterator iter = otherTraversal.deepFolders.iterator(); iter.hasNext();) {
+ IResource resource = (IResource) iter.next();
+ if (!isCovered(resource, IResource.DEPTH_INFINITE)) {
+ result.add(resource);
+ }
+ }
+ return result.toArray(new IResource[result.size()]);
+ }
+
+ public synchronized ResourceTraversal[] asTraversals() {
+ List<ResourceTraversal> result = new ArrayList<>();
+ if (!files.isEmpty() || ! zeroFolders.isEmpty()) {
+ Set<IResource> combined = new HashSet<>();
+ combined.addAll(files);
+ combined.addAll(zeroFolders);
+ result.add(new ResourceTraversal(combined.toArray(new IResource[combined.size()]), IResource.DEPTH_ZERO, IResource.NONE));
+ }
+ if (!shallowFolders.isEmpty()) {
+ result.add(new ResourceTraversal(shallowFolders.toArray(new IResource[shallowFolders.size()]), IResource.DEPTH_ONE, IResource.NONE));
+ }
+ if (!deepFolders.isEmpty()) {
+ result.add(new ResourceTraversal(deepFolders.toArray(new IResource[deepFolders.size()]), IResource.DEPTH_INFINITE, IResource.NONE));
+ }
+ return result.toArray(new ResourceTraversal[result.size()]);
+ }
+
+ public synchronized IResource[] getRoots() {
+ List<IResource> result = new ArrayList<>();
+ result.addAll(files);
+ result.addAll(zeroFolders);
+ result.addAll(shallowFolders);
+ result.addAll(deepFolders);
+ return result.toArray(new IResource[result.size()]);
+ }
+
+ public synchronized ResourceTraversal[] getUncoveredTraversals(ResourceTraversal[] traversals) {
+ CompoundResourceTraversal other = new CompoundResourceTraversal();
+ other.addTraversals(traversals);
+ return getUncoveredTraversals(other);
+ }
+
+ public ResourceTraversal[] getUncoveredTraversals(CompoundResourceTraversal otherTraversal) {
+ synchronized (otherTraversal) {
+ CompoundResourceTraversal uncovered = new CompoundResourceTraversal();
+ for (Iterator iter = otherTraversal.files.iterator(); iter.hasNext();) {
+ IResource resource = (IResource) iter.next();
+ if (!isCovered(resource, IResource.DEPTH_ZERO)) {
+ uncovered.addResource(resource, IResource.DEPTH_ZERO);
+ }
+ }
+ for (Iterator iter = otherTraversal.zeroFolders.iterator(); iter.hasNext();) {
+ IResource resource = (IResource) iter.next();
+ if (!isCovered(resource, IResource.DEPTH_ZERO)) {
+ uncovered.addResource(resource, IResource.DEPTH_ZERO);
+ }
+ }
+ for (Iterator iter = otherTraversal.shallowFolders.iterator(); iter.hasNext();) {
+ IResource resource = (IResource) iter.next();
+ if (!isCovered(resource, IResource.DEPTH_ONE)) {
+ uncovered.addResource(resource, IResource.DEPTH_ONE);
+ }
+ }
+ for (Iterator iter = otherTraversal.deepFolders.iterator(); iter.hasNext();) {
+ IResource resource = (IResource) iter.next();
+ if (!isCovered(resource, IResource.DEPTH_INFINITE)) {
+ uncovered.addResource(resource, IResource.DEPTH_INFINITE);
+ }
+ }
+ return uncovered.asTraversals();
+ }
+ }
+
+ public synchronized void clear() {
+ deepFolders.clear();
+ shallowFolders.clear();
+ zeroFolders.clear();
+ files.clear();
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/DiffChangeEvent.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/DiffChangeEvent.java
new file mode 100644
index 000000000..6fb7515bf
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/DiffChangeEvent.java
@@ -0,0 +1,116 @@
+/*******************************************************************************
+ * 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.internal.core.mapping;
+
+import java.util.*;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.team.core.diff.*;
+
+/**
+ * Implementation of {@link IDiffChangeEvent}
+ */
+public class DiffChangeEvent implements IDiffChangeEvent {
+
+ private final IDiffTree tree;
+
+ // List that accumulate changes
+ // SyncInfo
+ private Map<IPath, IDiff> changedResources = new HashMap<>();
+ private Set<IPath> removedResources = new HashSet<>();
+ private Map<IPath, IDiff> addedResources = new HashMap<>();
+
+ private boolean reset = false;
+
+ private List<IStatus> errors = new ArrayList<>();
+
+ /**
+ * Create a diff change event
+ * @param tree the originating tree
+ */
+ public DiffChangeEvent(IDiffTree tree) {
+ this.tree = tree;
+ }
+
+ @Override
+ public IDiffTree getTree() {
+ return tree;
+ }
+
+ @Override
+ public IDiff[] getAdditions() {
+ return addedResources.values().toArray(new IDiff[addedResources.size()]);
+ }
+
+ @Override
+ public IPath[] getRemovals() {
+ return removedResources.toArray(new IPath[removedResources.size()]);
+ }
+
+ @Override
+ public IDiff[] getChanges() {
+ return changedResources.values().toArray(new IDiff[changedResources.size()]);
+ }
+
+ public void added(IDiff delta) {
+ if (removedResources.contains(delta.getPath())) {
+ // A removal followed by an addition is treated as a change
+ removedResources.remove(delta.getPath());
+ changed(delta);
+ } else {
+ addedResources.put(delta.getPath(), delta);
+ }
+ }
+
+ public void removed(IPath path, IDiff delta) {
+ if (changedResources.containsKey(path)) {
+ // No use in reporting the change since it has subsequently been removed
+ changedResources.remove(path);
+ } else if (addedResources.containsKey(path)) {
+ // An addition followed by a removal can be dropped
+ addedResources.remove(path);
+ return;
+ }
+ removedResources.add(path);
+ }
+
+ public void changed(IDiff delta) {
+ if (addedResources.containsKey(delta.getPath())) {
+ // An addition followed by a change is an addition
+ addedResources.put(delta.getPath(), delta);
+ return;
+ }
+ changedResources.put(delta.getPath(), delta);
+ }
+
+ public void reset() {
+ reset = true;
+ }
+
+ public boolean isReset() {
+ return reset;
+ }
+
+ public boolean isEmpty() {
+ return changedResources.isEmpty() && removedResources.isEmpty() && addedResources.isEmpty();
+ }
+
+ public void errorOccurred(IStatus status) {
+ errors .add(status);
+ }
+
+ @Override
+ public IStatus[] getErrors() {
+ return errors.toArray(new IStatus[errors.size()]);
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/GroupProgressMonitor.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/GroupProgressMonitor.java
new file mode 100644
index 000000000..7c96e011c
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/GroupProgressMonitor.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2006 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.internal.core.mapping;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.ProgressMonitorWrapper;
+
+public class GroupProgressMonitor extends ProgressMonitorWrapper implements
+ IProgressMonitor {
+
+ private final IProgressMonitor group;
+ private final int ticks;
+
+ public GroupProgressMonitor(IProgressMonitor monitor, IProgressMonitor group, int groupTicks) {
+ super(monitor);
+ this.group = group;
+ this.ticks = groupTicks;
+ }
+
+ public IProgressMonitor getGroup() {
+ return group;
+ }
+
+ public int getTicks() {
+ return ticks;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/IStreamMergerDelegate.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/IStreamMergerDelegate.java
new file mode 100644
index 000000000..fd8344eab
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/IStreamMergerDelegate.java
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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.internal.core.mapping;
+
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.team.core.mapping.DelegatingStorageMerger;
+import org.eclipse.team.core.mapping.IStorageMerger;
+
+/**
+ * Interface that allows the {@link DelegatingStorageMerger} to
+ * delegate to IStreamMergers. We need an interface so the UI can
+ * provide the implementation.
+ */
+public interface IStreamMergerDelegate {
+ /**
+ * Find a storage merger for the given target.
+ * A storage merger will only be returned if
+ * there is a stream merger that matches the
+ * targets content type or extension.
+ *
+ * @param target the input storage
+ * @return a storage merger for the given target
+ */
+ IStorageMerger findMerger(IStorage target);
+} \ No newline at end of file
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/LineComparator.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/LineComparator.java
new file mode 100644
index 000000000..21e1ff04c
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/LineComparator.java
@@ -0,0 +1,145 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 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.internal.core.mapping;
+
+import java.io.*;
+import java.util.ArrayList;
+
+import org.eclipse.compare.rangedifferencer.IRangeComparator;
+import org.eclipse.core.resources.IEncodedStorage;
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.CoreException;
+
+/**
+ * This implementation of IRangeComparator breaks an input stream into lines.
+ * Copied from org.eclipse.compare.internal.merge.LineComparator 1.4 and
+ * modified for {@link IStorage}.
+ */
+class LineComparator implements IRangeComparator {
+
+ private String[] fLines;
+
+ /*
+ * An input stream reader that detects a trailing LF in the wrapped stream.
+ */
+ private static class TrailingLineFeedDetector extends FilterInputStream {
+
+ boolean trailingLF = false;
+
+ protected TrailingLineFeedDetector(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public int read() throws IOException {
+ int c = super.read();
+ trailingLF = isLineFeed(c);
+ return c;
+ }
+
+ /*
+ * We don't need to override read(byte[] buffer) as the javadoc of
+ * FilterInputStream states that it will call read(byte[] buffer, int
+ * off, int len)
+ */
+ @Override
+ public int read(byte[] buffer, int off, int len) throws IOException {
+ int length = super.read(buffer, off, len);
+ if (length != -1) {
+ int index = off + length - 1;
+ if (index >= buffer.length)
+ index = buffer.length - 1;
+ trailingLF = isLineFeed(buffer[index]);
+ }
+ return length;
+ }
+
+ private boolean isLineFeed(int c) {
+ return c != -1 && c == '\n';
+ }
+
+ public boolean hadTrailingLineFeed() {
+ return trailingLF;
+ }
+
+ }
+
+ public static LineComparator create(IStorage storage, String outputEncoding)
+ throws CoreException, IOException {
+ InputStream is = new BufferedInputStream(storage.getContents());
+ try {
+ String encoding = getEncoding(storage, outputEncoding);
+ return new LineComparator(is, encoding);
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+
+ private static String getEncoding(IStorage storage, String outputEncoding)
+ throws CoreException {
+ if (storage instanceof IEncodedStorage) {
+ IEncodedStorage es = (IEncodedStorage) storage;
+ String charset = es.getCharset();
+ if (charset != null)
+ return charset;
+ }
+ return outputEncoding;
+ }
+
+ public LineComparator(InputStream is, String encoding) throws IOException {
+
+ TrailingLineFeedDetector trailingLineFeedDetector = new TrailingLineFeedDetector(
+ is);
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(
+ trailingLineFeedDetector, encoding))) {
+ String line;
+ ArrayList<String> ar = new ArrayList<>();
+ while ((line = br.readLine()) != null) {
+ ar.add(line);
+ }
+ // Add a trailing line if the last character in the file was a line
+ // feed.
+ // We do this because a BufferedReader doesn't distinguish the case
+ // where the last line has or doesn't have a trailing line separator
+ if (trailingLineFeedDetector.hadTrailingLineFeed()) {
+ ar.add(""); //$NON-NLS-1$
+ }
+ fLines = ar.toArray(new String[ar.size()]);
+ }
+ }
+
+ String getLine(int ix) {
+ return fLines[ix];
+ }
+
+ @Override
+ public int getRangeCount() {
+ return fLines.length;
+ }
+
+ @Override
+ public boolean rangesEqual(int thisIndex, IRangeComparator other,
+ int otherIndex) {
+ String s1 = fLines[thisIndex];
+ String s2 = ((LineComparator) other).fLines[otherIndex];
+ return s1.equals(s2);
+ }
+
+ @Override
+ public boolean skipRangeComparison(int length, int maxLength,
+ IRangeComparator other) {
+ return false;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/LocalResourceVariant.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/LocalResourceVariant.java
new file mode 100644
index 000000000..36e7af7f8
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/LocalResourceVariant.java
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * 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.internal.core.mapping;
+
+import java.util.Date;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.variants.IResourceVariant;
+
+public class LocalResourceVariant implements IResourceVariant {
+ private final IResource resource;
+
+ public LocalResourceVariant(IResource resource) {
+ this.resource = resource;
+ }
+
+ @Override
+ public byte[] asBytes() {
+ return getContentIdentifier().getBytes();
+ }
+
+ @Override
+ public String getContentIdentifier() {
+ return new Date(resource.getLocalTimeStamp()).toString();
+ }
+
+ @Override
+ public IStorage getStorage(IProgressMonitor monitor) throws TeamException {
+ if (resource.getType() == IResource.FILE) {
+ return (IFile)resource;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isContainer() {
+ return resource.getType() != IResource.FILE;
+ }
+
+ @Override
+ public String getName() {
+ return resource.getName();
+ }
+} \ No newline at end of file
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ModelProviderResourceMapping.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ModelProviderResourceMapping.java
new file mode 100644
index 000000000..487e04a6d
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ModelProviderResourceMapping.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.mapping;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.resources.mapping.*;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.internal.core.Policy;
+import org.eclipse.team.internal.core.TeamPlugin;
+
+public class ModelProviderResourceMapping extends ResourceMapping {
+
+ ModelProvider provider;
+
+ public ModelProviderResourceMapping(ModelProvider provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ public Object getModelObject() {
+ return provider;
+ }
+
+ @Override
+ public String getModelProviderId() {
+ // Use the resource model provider id. Model providers
+ // can override this by adapting their specific model provider class
+ return ModelProvider.RESOURCE_MODEL_PROVIDER_ID;
+ }
+
+ @Override
+ public IProject[] getProjects() {
+ IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
+ try {
+ IResource[] resources = provider.getDescriptor().getMatchingResources(projects);
+ Set<IProject> result = new HashSet<>();
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ if (resource.isAccessible())
+ result.add(resource.getProject());
+ }
+ return result.toArray(new IProject[result.size()]);
+ } catch (CoreException e) {
+ TeamPlugin.log(e);
+ }
+ return projects;
+ }
+
+ @Override
+ public ResourceTraversal[] getTraversals(ResourceMappingContext context,
+ IProgressMonitor monitor) throws CoreException {
+ monitor = Policy.monitorFor(monitor);
+ try {
+ monitor.beginTask(null, 100);
+ ResourceMapping[] mappings = provider.getMappings(getProviderResources(context), context, Policy.subMonitorFor(monitor, 50));
+ return provider.getTraversals(mappings, context, Policy.subMonitorFor(monitor, 50));
+ } finally {
+ monitor.done();
+ }
+ }
+
+ private IResource[] getProviderResources(ResourceMappingContext context) {
+ try {
+ if (context instanceof RemoteResourceMappingContext) {
+ RemoteResourceMappingContext rrmc = (RemoteResourceMappingContext) context;
+ return provider.getDescriptor().getMatchingResources(rrmc.getProjects());
+ }
+ } catch (CoreException e) {
+ TeamPlugin.log(e);
+ }
+ return getProjects();
+ }
+
+ @Override
+ public boolean contains(ResourceMapping mapping) {
+ return (mapping.getModelProviderId().equals(getModelProviderId()));
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/PathTree.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/PathTree.java
new file mode 100644
index 000000000..61ff6d870
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/PathTree.java
@@ -0,0 +1,331 @@
+/*******************************************************************************
+ * 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.internal.core.mapping;
+
+import java.util.*;
+
+import org.eclipse.core.runtime.IPath;
+
+/**
+ * A tree of objects keyed by path
+ */
+public class PathTree {
+
+ class Node {
+ Object payload;
+ Set<IPath> descendantsWithPayload;
+ int flags;
+ public boolean isEmpty() {
+ return payload == null && (descendantsWithPayload == null || descendantsWithPayload.isEmpty());
+ }
+ public Object getPayload() {
+ return payload;
+ }
+ public void setPayload(Object payload) {
+ this.payload = payload;
+ }
+ public boolean hasDescendants() {
+ return descendantsWithPayload != null && !descendantsWithPayload.isEmpty();
+ }
+ public boolean hasFlag(int propertyBit) {
+ return (flags & propertyBit) != 0;
+ }
+ public void setProperty(int propertyBit, boolean value) {
+ if (value)
+ flags |= propertyBit;
+ else
+ flags ^= propertyBit;
+ }
+ public boolean descendantHasFlag(int property) {
+ if (hasDescendants()) {
+ for (Iterator<IPath> iter = descendantsWithPayload.iterator(); iter.hasNext();) {
+ IPath path = iter.next();
+ Node child = getNode(path);
+ if (child.hasFlag(property)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ private Map<IPath, Node> objects = new HashMap<>();
+
+ /**
+ * Return the object at the given path or <code>null</code>
+ * if there is no object at that path
+ * @param path the path
+ * @return the object at the given path or <code>null</code>
+ */
+ public synchronized Object get(IPath path) {
+ Node node = getNode(path);
+ if (node == null)
+ return null;
+ return node.getPayload();
+ }
+
+ /**
+ * Put the object at the given path. Return the
+ * previous object at that path or <code>null</code>
+ * if the path did not previously have an object.
+ * @param path the path of the object
+ * @param object the object
+ * @return the previous object at that path or <code>null</code>
+ */
+ public synchronized Object put(IPath path, Object object) {
+ Node node = getNode(path);
+ if (node == null) {
+ node = addNode(path);
+ }
+ Object previous = node.getPayload();
+ node.setPayload(object);
+ if(previous == null) {
+ addToParents(path, path);
+ }
+ return previous;
+ }
+
+ /**
+ * Remove the object at the given path and return
+ * the removed object or <code>null</code> if no
+ * object was removed.
+ * @param path the path to remove
+ * @return the removed object at the given path and return
+ * the removed object or <code>null</code>
+ */
+ public synchronized Object remove(IPath path) {
+ Node node = getNode(path);
+ if (node == null)
+ return null;
+ Object previous = node.getPayload();
+ node.setPayload(null);
+ if(previous != null) {
+ removeFromParents(path, path);
+ if (node.isEmpty()) {
+ removeNode(path);
+ }
+ }
+ return previous;
+
+ }
+
+ /**
+ * Return whether the given path has children in the tree
+ * @param path
+ * @return whether there are children for the given path
+ */
+ public synchronized boolean hasChildren(IPath path) {
+ if (path.isEmpty()) return !objects.isEmpty();
+ Node node = getNode(path);
+ if (node == null)
+ return false;
+ return node.hasDescendants();
+ }
+
+ /**
+ * Return the paths for any children of the given path in this set.
+ * @param path the path
+ * @return the paths for any children of the given path in this set
+ */
+ public synchronized IPath[] getChildren(IPath path) {
+ // OPTIMIZE: could be optimized so that we don't traverse all the deep
+ // children to find the immediate ones.
+ Set<IPath> children = new HashSet<>();
+ Node node = getNode(path);
+ if (node != null) {
+ Set possibleChildren = node.descendantsWithPayload;
+ if(possibleChildren != null) {
+ for (Iterator it = possibleChildren.iterator(); it.hasNext();) {
+ Object next = it.next();
+ IPath descendantPath = (IPath)next;
+ IPath childPath = null;
+ if(descendantPath.segmentCount() == (path.segmentCount() + 1)) {
+ childPath = descendantPath;
+ } else if (descendantPath.segmentCount() > path.segmentCount()) {
+ childPath = descendantPath.removeLastSegments(descendantPath.segmentCount() - path.segmentCount() - 1);
+ }
+ if (childPath != null) {
+ children.add(childPath);
+ }
+ }
+ }
+ }
+ return children.toArray(new IPath[children.size()]);
+ }
+
+ private boolean addToParents(IPath path, IPath parent) {
+ // this flag is used to indicate if the parent was previously in the set
+ boolean addedParent = false;
+ if (path == parent) {
+ // this is the leaf that was just added
+ addedParent = true;
+ } else {
+ Node node = getNode(parent);
+ if (node == null)
+ node = addNode(parent);
+ Set<IPath> children = node.descendantsWithPayload;
+ if (children == null) {
+ children = new HashSet<>();
+ node.descendantsWithPayload = children;
+ // this is a new folder in the sync set
+ addedParent = true;
+ }
+ children.add(path);
+ }
+ // if the parent already existed and the resource is new, record it
+ if ((parent.segmentCount() == 0 || !addToParents(path, parent.removeLastSegments(1))) && addedParent) {
+ // TODO: we may not need to record the removed subtree
+ // internalAddedSubtreeRoot(parent);
+ }
+ return addedParent;
+ }
+
+ private boolean removeFromParents(IPath path, IPath parent) {
+ // this flag is used to indicate if the parent was removed from the set
+ boolean removedParent = false;
+ Node node = getNode(parent);
+ if (node == null) {
+ // this is the leaf
+ removedParent = true;
+ } else {
+ Set children = node.descendantsWithPayload;
+ if (children == null) {
+ // this is the leaf
+ removedParent = true;
+ } else {
+ children.remove(path);
+ if (children.isEmpty()) {
+ node.descendantsWithPayload = null;
+ if (node.isEmpty())
+ removeNode(parent);
+ removedParent = true;
+ }
+ }
+ }
+ // if the parent wasn't removed and the resource was, record it
+ if ((parent.segmentCount() == 0 || !removeFromParents(path, parent.removeLastSegments(1))) && removedParent) {
+ // TODO: may not need to record this
+ //internalRemovedSubtreeRoot(parent);
+ }
+ return removedParent;
+ }
+
+ /**
+ * Clear all entries from the path tree.
+ */
+ public synchronized void clear() {
+ objects.clear();
+ }
+
+ /**
+ * Return whether the path tree is empty.
+ * @return whether the path tree is empty
+ */
+ public synchronized boolean isEmpty() {
+ return objects.isEmpty();
+ }
+
+ /**
+ * Return the paths in this tree that contain diffs.
+ * @return the paths in this tree that contain diffs.
+ */
+ public synchronized IPath[] getPaths() {
+ List<IPath> result = new ArrayList<>();
+ for (Iterator iter = objects.keySet().iterator(); iter.hasNext();) {
+ IPath path = (IPath) iter.next();
+ Node node = getNode(path);
+ if (node.getPayload() != null)
+ result.add(path);
+ }
+ return result.toArray(new IPath[result.size()]);
+ }
+
+ /**
+ * Return all the values contained in this path tree.
+ * @return all the values in the tree
+ */
+ public synchronized Collection values() {
+ List<Object> result = new ArrayList<>();
+ for (Iterator iter = objects.keySet().iterator(); iter.hasNext();) {
+ IPath path = (IPath) iter.next();
+ Node node = getNode(path);
+ if (node.getPayload() != null)
+ result.add(node.getPayload());
+ }
+ return result;
+ }
+
+ /**
+ * Return the number of nodes contained in this path tree.
+ * @return the number of nodes contained in this path tree
+ */
+ public int size() {
+ return values().size();
+ }
+
+ private Node getNode(IPath path) {
+ return objects.get(path);
+ }
+
+ private Node addNode(IPath path) {
+ Node node;
+ node = new Node();
+ objects.put(path, node);
+ return node;
+ }
+
+ private Object removeNode(IPath path) {
+ return objects.remove(path);
+ }
+
+ /**
+ * Set the property for the given path and propogate the
+ * bit to the root. The property is only set if the given path
+ * already exists in the tree.
+ * @param path the path
+ * @param property the property bit to set
+ * @param value whether the bit should be on or off
+ * @return the paths whose bit changed
+ */
+ public synchronized IPath[] setPropogatedProperty(IPath path, int property, boolean value) {
+ Set<IPath> changed = new HashSet<>();
+ internalSetPropertyBit(path, property, value, changed);
+ return changed.toArray(new IPath[changed.size()]);
+ }
+
+ private void internalSetPropertyBit(IPath path, int property, boolean value, Set<IPath> changed) {
+ if (path.segmentCount() == 0)
+ return;
+ Node node = getNode(path);
+ if (node == null)
+ return;
+ // No need to set it if the value hans't changed
+ if (value == node.hasFlag(property))
+ return;
+ // Only unset the property if no descendants have the flag set
+ if (!value && node.descendantHasFlag(property))
+ return;
+ node.setProperty(property, value);
+ changed.add(path);
+ internalSetPropertyBit(path.removeLastSegments(1), property, value, changed);
+ }
+
+ public synchronized boolean getProperty(IPath path, int property) {
+ if (path.segmentCount() == 0)
+ return false;
+ Node node = getNode(path);
+ if (node == null)
+ return false;
+ return (node.hasFlag(property));
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceMappingInputScope.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceMappingInputScope.java
new file mode 100644
index 000000000..042b22152
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceMappingInputScope.java
@@ -0,0 +1,100 @@
+/*******************************************************************************
+ * 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.internal.core.mapping;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.mapping.*;
+import org.eclipse.team.core.mapping.ISynchronizationScope;
+
+/**
+ * This scope wraps another scope and treats the input mappings of
+ * that wrapped scope as the mappings of this scope.
+ */
+public class ResourceMappingInputScope extends AbstractResourceMappingScope {
+
+ ISynchronizationScope wrappedScope;
+
+ public ResourceMappingInputScope(ISynchronizationScope wrappedScope) {
+
+ this.wrappedScope = wrappedScope;
+ }
+
+ @Override
+ public ResourceMapping[] getInputMappings() {
+ return wrappedScope.getInputMappings();
+ }
+
+ @Override
+ public ResourceMapping[] getMappings() {
+ return getInputMappings();
+ }
+
+ @Override
+ public ResourceTraversal[] getTraversals() {
+ CompoundResourceTraversal result = new CompoundResourceTraversal();
+ ResourceMapping[] mappings = getMappings();
+ for (int i = 0; i < mappings.length; i++) {
+ ResourceMapping mapping = mappings[i];
+ ResourceTraversal[] traversals = getTraversals(mapping);
+ result.addTraversals(traversals);
+ }
+ return result.asTraversals();
+ }
+
+ @Override
+ public ResourceTraversal[] getTraversals(ResourceMapping mapping) {
+ if (!contains(mapping)) {
+ return null;
+ }
+ return wrappedScope.getTraversals(mapping);
+ }
+
+ private boolean contains(ResourceMapping mapping) {
+ ResourceMapping[] mappings = getMappings();
+ for (int i = 0; i < mappings.length; i++) {
+ ResourceMapping child = mappings[i];
+ if (child.equals(mapping)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean hasAdditionalMappings() {
+ return false;
+ }
+
+ @Override
+ public boolean hasAdditonalResources() {
+ return false;
+ }
+
+ @Override
+ public ISynchronizationScope asInputScope() {
+ return this;
+ }
+
+ @Override
+ public IProject[] getProjects() {
+ return wrappedScope.getProjects();
+ }
+
+ @Override
+ public ResourceMappingContext getContext() {
+ return wrappedScope.getContext();
+ }
+
+ @Override
+ public void refresh(ResourceMapping[] mappings) {
+ wrappedScope.refresh(mappings);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceMappingScope.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceMappingScope.java
new file mode 100644
index 000000000..d94e4b535
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceMappingScope.java
@@ -0,0 +1,143 @@
+/*******************************************************************************
+ * 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.internal.core.mapping;
+
+import java.util.*;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.resources.mapping.*;
+import org.eclipse.team.core.mapping.ISynchronizationScope;
+import org.eclipse.team.core.mapping.provider.SynchronizationScopeManager;
+
+/**
+ * Concrete implementation of the {@link ISynchronizationScope} interface for
+ * use by clients.
+ *
+ * @see org.eclipse.core.resources.mapping.ResourceMapping
+ *
+ * @since 3.2
+ * @noextend This class is not intended to be subclassed by clients.
+ */
+public class ResourceMappingScope extends AbstractResourceMappingScope {
+
+ private ResourceMapping[] inputMappings;
+ private final Map<ResourceMapping, ResourceTraversal[]> mappingsToTraversals = Collections.synchronizedMap(new HashMap<>());
+ private boolean hasAdditionalMappings;
+ private boolean hasAdditionalResources;
+ private final CompoundResourceTraversal compoundTraversal = new CompoundResourceTraversal();
+ private final SynchronizationScopeManager manager;
+
+ public ResourceMappingScope(ResourceMapping[] selectedMappings, SynchronizationScopeManager manager) {
+ inputMappings = selectedMappings;
+ this.manager = manager;
+ }
+
+ /**
+ * Add the mapping and its traversals to the scope. This method should
+ * only be invoked during the scope building process.
+ * @param mapping the mapping being added to the scope
+ * @param traversals the traversals for that mapping
+ * @return the added traversals
+ */
+ public ResourceTraversal[] addMapping(ResourceMapping mapping, ResourceTraversal[] traversals) {
+ ResourceTraversal[] newTraversals = compoundTraversal.getUncoveredTraversals(traversals);
+ mappingsToTraversals.put(mapping, traversals);
+ compoundTraversal.addTraversals(traversals);
+ return newTraversals;
+ }
+
+ @Override
+ public ResourceMapping[] getInputMappings() {
+ return inputMappings;
+ }
+
+ @Override
+ public ResourceMapping[] getMappings() {
+ if (mappingsToTraversals.isEmpty())
+ return inputMappings;
+ return mappingsToTraversals.keySet().toArray(new ResourceMapping[mappingsToTraversals.size()]);
+ }
+
+ @Override
+ public ResourceTraversal[] getTraversals() {
+ return compoundTraversal.asTraversals();
+ }
+
+ @Override
+ public ResourceTraversal[] getTraversals(ResourceMapping mapping) {
+ return mappingsToTraversals.get(mapping);
+ }
+
+ @Override
+ public boolean hasAdditionalMappings() {
+ return hasAdditionalMappings;
+ }
+
+ /**
+ * Set whether the scope has additional mappings to the input mappings.
+ * This method should only be invoked during the scope building process.
+ * @param hasAdditionalMappings whether the scope has additional mappings
+ */
+ public void setHasAdditionalMappings(boolean hasAdditionalMappings) {
+ this.hasAdditionalMappings = hasAdditionalMappings;
+ }
+
+ /**
+ * Set whether this scope has additional resources.
+ * This method should only be invoked during the scope building process.
+ * @param hasAdditionalResources whether the scope has additional resources
+ */
+ public void setHasAdditionalResources(boolean hasAdditionalResources) {
+ this.hasAdditionalResources = hasAdditionalResources;
+ }
+
+ @Override
+ public boolean hasAdditonalResources() {
+ return hasAdditionalResources;
+ }
+
+ public CompoundResourceTraversal getCompoundTraversal() {
+ return compoundTraversal;
+ }
+
+ @Override
+ public ISynchronizationScope asInputScope() {
+ return new ResourceMappingInputScope(this);
+ }
+
+ @Override
+ public IProject[] getProjects() {
+ ResourceMappingContext context = getContext();
+ if (context instanceof RemoteResourceMappingContext) {
+ RemoteResourceMappingContext rrmc = (RemoteResourceMappingContext) context;
+ return rrmc.getProjects();
+ }
+ return ResourcesPlugin.getWorkspace().getRoot().getProjects();
+ }
+
+ @Override
+ public ResourceMappingContext getContext() {
+ return manager.getContext();
+ }
+
+ @Override
+ public void refresh(ResourceMapping[] mappings) {
+ manager.refresh(mappings);
+ }
+
+ public void reset() {
+ mappingsToTraversals.clear();
+ compoundTraversal.clear();
+ hasAdditionalMappings = false;
+ hasAdditionalResources = false;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceVariantFileRevision.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceVariantFileRevision.java
new file mode 100644
index 000000000..3f1ebf227
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ResourceVariantFileRevision.java
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.mapping;
+
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.history.IFileRevision;
+import org.eclipse.team.core.history.provider.FileRevision;
+import org.eclipse.team.core.variants.IResourceVariant;
+
+public class ResourceVariantFileRevision extends FileRevision implements IAdaptable {
+ private final IResourceVariant variant;
+
+ public ResourceVariantFileRevision(IResourceVariant variant) {
+ this.variant = variant;
+ }
+
+ @Override
+ public IStorage getStorage(IProgressMonitor monitor) throws CoreException {
+ return variant.getStorage(monitor);
+ }
+
+ @Override
+ public String getName() {
+ return variant.getName();
+ }
+
+ @Override
+ public String getContentIdentifier() {
+ return variant.getContentIdentifier();
+ }
+
+ public IResourceVariant getVariant() {
+ return variant;
+ }
+
+ @Override
+ public boolean isPropertyMissing() {
+ return false;
+ }
+
+ @Override
+ public IFileRevision withAllProperties(IProgressMonitor monitor) throws CoreException {
+ return this;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> T getAdapter(Class<T> adapter) {
+ if (adapter == IResourceVariant.class)
+ return (T) variant;
+ Object object = Platform.getAdapterManager().getAdapter(this, adapter);
+ if (object != null)
+ return (T) object;
+ if (variant instanceof IAdaptable ) {
+ IAdaptable adaptable = (IAdaptable ) variant;
+ return adaptable.getAdapter(adapter);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ResourceVariantFileRevision) {
+ ResourceVariantFileRevision fileRevision = (ResourceVariantFileRevision) obj;
+ return fileRevision.getVariant().equals(getVariant());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return getVariant().hashCode();
+ }
+} \ No newline at end of file
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ScopeChangeEvent.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ScopeChangeEvent.java
new file mode 100644
index 000000000..11a3a5cd4
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ScopeChangeEvent.java
@@ -0,0 +1,115 @@
+/*******************************************************************************
+ * Copyright (c) 2007, 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.internal.core.mapping;
+
+import java.util.*;
+
+import org.eclipse.core.resources.mapping.ResourceMapping;
+import org.eclipse.core.resources.mapping.ResourceTraversal;
+import org.eclipse.team.core.mapping.ISynchronizationScope;
+
+public class ScopeChangeEvent {
+
+ private final ISynchronizationScope scope;
+ private final ResourceMapping[] originalMappings;
+ private final ResourceTraversal[] originalTraversals;
+ private boolean expanded;
+ private boolean contracted;
+
+ public ScopeChangeEvent(ISynchronizationScope scope) {
+ this.scope = scope;
+ originalMappings = scope.getMappings();
+ originalTraversals = scope.getTraversals();
+ }
+
+ public boolean hasAdditionalMappings() {
+ return scope.getMappings().length > originalMappings.length;
+ }
+
+ public ResourceTraversal[] getUncoveredTraversals(CompoundResourceTraversal traversal) {
+ CompoundResourceTraversal originals = new CompoundResourceTraversal();
+ originals.addTraversals(originalTraversals);
+ return originals.getUncoveredTraversals(traversal);
+ }
+
+ public void setExpanded(boolean expanded) {
+ this.expanded = expanded;
+ }
+
+ public boolean isExpanded() {
+ return expanded;
+ }
+
+ public void setContracted(boolean contracted) {
+ this.contracted = contracted;
+ }
+
+ public boolean isContracted() {
+ return contracted;
+ }
+
+ public ResourceMapping[] getChangedMappings() {
+ ResourceMapping[] currentMappings = scope.getMappings();
+ ResourceMapping[] changedMappings;
+ if (currentMappings.length > originalMappings.length) {
+ // The number of mappings has increased so we should report the new mappings
+ Set<ResourceMapping> originalSet = new HashSet<>();
+ List<ResourceMapping> result = new ArrayList<>();
+ for (int i = 0; i < originalMappings.length; i++) {
+ ResourceMapping mapping = originalMappings[i];
+ originalSet.add(mapping);
+ }
+ for (int i = 0; i < currentMappings.length; i++) {
+ ResourceMapping mapping = currentMappings[i];
+ if (!originalSet.contains(mapping)) {
+ result.add(mapping);
+ }
+ }
+ changedMappings = result.toArray(new ResourceMapping[result.size()]);
+ } else if (isContracted()) {
+ // The number of mappings may be smaller so report the removed mappings
+ Set<ResourceMapping> finalSet = new HashSet<>();
+ List<ResourceMapping> result = new ArrayList<>();
+ for (int i = 0; i < currentMappings.length; i++) {
+ ResourceMapping mapping = currentMappings[i];
+ finalSet.add(mapping);
+ }
+ for (int i = 0; i < originalMappings.length; i++) {
+ ResourceMapping mapping = originalMappings[i];
+ if (!finalSet.contains(mapping)) {
+ result.add(mapping);
+ }
+ }
+ changedMappings = result.toArray(new ResourceMapping[result.size()]);
+ } else {
+ changedMappings = new ResourceMapping[0];
+ }
+ return changedMappings;
+ }
+
+ public ResourceTraversal[] getChangedTraversals(CompoundResourceTraversal refreshTraversals) {
+ ResourceTraversal[] changesTraversals;
+ if (isExpanded()) {
+ changesTraversals = getUncoveredTraversals(refreshTraversals);
+ } else if (isContracted()) {
+ CompoundResourceTraversal finalTraversals = new CompoundResourceTraversal();
+ finalTraversals.addTraversals(scope.getTraversals());
+ changesTraversals = finalTraversals.getUncoveredTraversals(originalTraversals);
+ } else {
+ changesTraversals = new ResourceTraversal[0];
+ }
+ return changesTraversals;
+ }
+
+ public boolean shouldFireChange() {
+ return isExpanded() || isContracted() || hasAdditionalMappings();
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ScopeManagerEventHandler.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ScopeManagerEventHandler.java
new file mode 100644
index 000000000..3fdc4906d
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/ScopeManagerEventHandler.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.mapping;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.core.resources.mapping.ResourceMapping;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.mapping.ISynchronizationScopeManager;
+import org.eclipse.team.core.mapping.provider.SynchronizationScopeManager;
+import org.eclipse.team.internal.core.BackgroundEventHandler;
+import org.eclipse.team.internal.core.Messages;
+
+public class ScopeManagerEventHandler extends BackgroundEventHandler {
+
+ public static final int REFRESH = 10;
+ private Set<ResourceMapping> toRefresh = new HashSet<>();
+ private ISynchronizationScopeManager manager;
+
+ class ResourceMappingEvent extends Event {
+ private final ResourceMapping[] mappings;
+ public ResourceMappingEvent(ResourceMapping[] mappings) {
+ super(REFRESH);
+ this.mappings = mappings;
+ }
+ }
+
+ public ScopeManagerEventHandler(SynchronizationScopeManager manager) {
+ super(NLS.bind(Messages.ScopeManagerEventHandler_0, manager.getName()), NLS.bind(Messages.ScopeManagerEventHandler_1, manager.getName()));
+ this.manager = manager;
+ }
+
+ @Override
+ protected boolean doDispatchEvents(IProgressMonitor monitor)
+ throws TeamException {
+ ResourceMapping[] mappings = toRefresh.toArray(new ResourceMapping[toRefresh.size()]);
+ toRefresh.clear();
+ if (mappings.length > 0) {
+ try {
+ manager.refresh(mappings, monitor);
+ } catch (CoreException e) {
+ throw TeamException.asTeamException(e);
+ }
+ }
+ return mappings.length > 0;
+ }
+
+ @Override
+ protected void processEvent(Event event, IProgressMonitor monitor)
+ throws CoreException {
+ if (event instanceof ResourceMappingEvent) {
+ ResourceMappingEvent rme = (ResourceMappingEvent) event;
+ for (int i = 0; i < rme.mappings.length; i++) {
+ ResourceMapping mapping = rme.mappings[i];
+ toRefresh.add(mapping);
+ }
+ }
+
+ }
+
+ public void refresh(ResourceMapping[] mappings) {
+ queueEvent(new ResourceMappingEvent(mappings), false);
+ }
+
+ @Override
+ protected Object getJobFamiliy() {
+ return manager;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/SyncInfoToDiffConverter.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/SyncInfoToDiffConverter.java
new file mode 100644
index 000000000..2bd474d23
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/SyncInfoToDiffConverter.java
@@ -0,0 +1,349 @@
+/*******************************************************************************
+ * 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.internal.core.mapping;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.diff.*;
+import org.eclipse.team.core.diff.provider.Diff;
+import org.eclipse.team.core.diff.provider.ThreeWayDiff;
+import org.eclipse.team.core.history.IFileRevision;
+import org.eclipse.team.core.mapping.IResourceDiff;
+import org.eclipse.team.core.mapping.provider.ResourceDiff;
+import org.eclipse.team.core.synchronize.SyncInfo;
+import org.eclipse.team.core.variants.IResourceVariant;
+import org.eclipse.team.core.variants.IResourceVariantComparator;
+import org.eclipse.team.internal.core.Messages;
+import org.eclipse.team.internal.core.history.LocalFileRevision;
+
+/**
+ * Covert a SyncInfo into a IDiff
+ */
+public class SyncInfoToDiffConverter {
+
+ private static class PrecalculatedSyncInfo extends SyncInfo {
+ public int kind;
+ public PrecalculatedSyncInfo(int kind, IResource local, IResourceVariant base, IResourceVariant remote, IResourceVariantComparator comparator) {
+ super(local, base, remote, comparator);
+ this.kind = kind;
+ }
+
+ @Override
+ protected int calculateKind() throws TeamException {
+ return kind;
+ }
+ }
+
+ private static SyncInfoToDiffConverter instance;
+
+
+ public static String diffKindToString(int kind) {
+ String label = ""; //$NON-NLS-1$
+ if(kind==IDiff.NO_CHANGE) {
+ label = Messages.RemoteSyncElement_insync;
+ } else {
+ switch(kind) {
+ case IDiff.CHANGE: label = Messages.RemoteSyncElement_change ; break;
+ case IDiff.ADD: label = Messages.RemoteSyncElement_addition; break;
+ case IDiff.REMOVE: label = Messages.RemoteSyncElement_deletion; break;
+ }
+ }
+ return label;
+ }
+
+ public static String diffDirectionToString(int direction) {
+ switch(direction) {
+ case IThreeWayDiff.CONFLICTING: return Messages.RemoteSyncElement_conflicting;
+ case IThreeWayDiff.OUTGOING: return Messages.RemoteSyncElement_outgoing;
+ case IThreeWayDiff.INCOMING: return Messages.RemoteSyncElement_incoming;
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+ public static String diffStatusToString(int status) {
+ int kind = status & Diff.KIND_MASK;
+ String label = diffKindToString(kind);
+ int direction = status & ThreeWayDiff.DIRECTION_MASK;
+ if (direction != 0)
+ label = NLS.bind(Messages.concatStrings, new String[] { diffDirectionToString(direction), label });
+ return label;
+ }
+
+ public static int asDiffFlags(int syncInfoFlags) {
+ if (syncInfoFlags == SyncInfo.IN_SYNC)
+ return IDiff.NO_CHANGE;
+ int kind = SyncInfo.getChange(syncInfoFlags);
+ int diffFlags = 0;
+ switch (kind) {
+ case SyncInfo.ADDITION:
+ diffFlags = IDiff.ADD;
+ break;
+ case SyncInfo.DELETION:
+ diffFlags = IDiff.REMOVE;
+ break;
+ case SyncInfo.CHANGE:
+ diffFlags = IDiff.CHANGE;
+ break;
+ }
+ int direction = SyncInfo.getDirection(syncInfoFlags);
+ switch (direction) {
+ case SyncInfo.INCOMING:
+ diffFlags |= IThreeWayDiff.INCOMING;
+ break;
+ case SyncInfo.OUTGOING:
+ diffFlags |= IThreeWayDiff.OUTGOING;
+ break;
+ case SyncInfo.CONFLICTING:
+ diffFlags |= IThreeWayDiff.CONFLICTING;
+ break;
+ }
+ return diffFlags;
+ }
+
+ private static int asSyncInfoKind(IThreeWayDiff diff) {
+ int kind = diff.getKind();
+ if (diff.getKind() == IDiff.NO_CHANGE)
+ return SyncInfo.IN_SYNC;
+ int syncKind = 0;
+ switch (kind) {
+ case IDiff.ADD:
+ syncKind = SyncInfo.ADDITION;
+ break;
+ case IDiff.REMOVE:
+ syncKind = SyncInfo.DELETION;
+ break;
+ case IDiff.CHANGE:
+ syncKind = SyncInfo.CHANGE;
+ break;
+ }
+ int direction = diff.getDirection();
+ switch (direction) {
+ case IThreeWayDiff.INCOMING:
+ syncKind |= SyncInfo.INCOMING;
+ break;
+ case IThreeWayDiff.OUTGOING:
+ syncKind |= SyncInfo.OUTGOING;
+ break;
+ case IThreeWayDiff.CONFLICTING:
+ syncKind |= SyncInfo.CONFLICTING;
+ break;
+ }
+ return syncKind;
+ }
+
+ public IDiff getDeltaFor(SyncInfo info) {
+ if (info.getComparator().isThreeWay()) {
+ ITwoWayDiff local = getLocalDelta(info);
+ ITwoWayDiff remote = getRemoteDelta(info);
+ return new ThreeWayDiff(local, remote);
+ } else {
+ if (info.getKind() != SyncInfo.IN_SYNC) {
+ IResourceVariant remote = info.getRemote();
+ IResource local = info.getLocal();
+ int kind;
+ if (remote == null) {
+ kind = IDiff.REMOVE;
+ } else if (!local.exists()) {
+ kind = IDiff.ADD;
+ } else {
+ kind = IDiff.CHANGE;
+ }
+ if (local.getType() == IResource.FILE) {
+ IFileRevision after = asFileState(remote);
+ IFileRevision before = getFileRevisionFor((IFile)local);
+ return new ResourceDiff(info.getLocal(), kind, 0, before, after);
+ }
+ // For folders, we don't need file states
+ return new ResourceDiff(info.getLocal(), kind);
+ }
+ return null;
+ }
+ }
+
+ private ITwoWayDiff getRemoteDelta(SyncInfo info) {
+ int direction = SyncInfo.getDirection(info.getKind());
+ if (direction == SyncInfo.INCOMING || direction == SyncInfo.CONFLICTING) {
+ IResourceVariant ancestor = info.getBase();
+ IResourceVariant remote = info.getRemote();
+ int kind;
+ if (ancestor == null) {
+ kind = IDiff.ADD;
+ } else if (remote == null) {
+ kind = IDiff.REMOVE;
+ } else {
+ kind = IDiff.CHANGE;
+ }
+ // For folders, we don't need file states
+ if (info.getLocal().getType() == IResource.FILE) {
+ IFileRevision before = asFileState(ancestor);
+ IFileRevision after = asFileState(remote);
+ return new ResourceDiff(info.getLocal(), kind, 0, before, after);
+ }
+
+ return new ResourceDiff(info.getLocal(), kind);
+ }
+ return null;
+ }
+
+ private IFileRevision asFileState(final IResourceVariant variant) {
+ if (variant == null)
+ return null;
+ return asFileRevision(variant);
+ }
+
+ private IFileRevision getFileRevisionFor(final IFile file) {
+ return new LocalFileRevision(file);
+ }
+
+ protected ResourceVariantFileRevision asFileRevision(final IResourceVariant variant) {
+ return new ResourceVariantFileRevision(variant);
+ }
+
+ private ITwoWayDiff getLocalDelta(SyncInfo info) {
+ int direction = SyncInfo.getDirection(info.getKind());
+ if (direction == SyncInfo.OUTGOING || direction == SyncInfo.CONFLICTING) {
+ IResourceVariant ancestor = info.getBase();
+ IResource local = info.getLocal();
+ int kind;
+ if (ancestor == null) {
+ kind = IDiff.ADD;
+ } else if (!local.exists()) {
+ kind = IDiff.REMOVE;
+ } else {
+ kind = IDiff.CHANGE;
+ }
+ if (local.getType() == IResource.FILE) {
+ IFileRevision before = asFileState(ancestor);
+ IFileRevision after = getFileRevisionFor((IFile)local);
+ return new ResourceDiff(info.getLocal(), kind, 0, before, after);
+ }
+ // For folders, we don't need file states
+ return new ResourceDiff(info.getLocal(), kind);
+
+ }
+ return null;
+ }
+
+ public static IResourceVariant getRemoteVariant(IThreeWayDiff twd) {
+ IFileRevision revision = getRemote(twd);
+ if (revision != null)
+ return asResourceVariant(revision);
+ return null;
+ }
+
+ public static IResourceVariant getBaseVariant(IThreeWayDiff twd) {
+ IResourceDiff diff = (IResourceDiff)twd.getRemoteChange();
+ if (diff != null)
+ return asResourceVariant(diff.getBeforeState());
+ diff = (IResourceDiff)twd.getLocalChange();
+ if (diff != null)
+ return asResourceVariant(diff.getBeforeState());
+ return null;
+ }
+
+ public SyncInfo asSyncInfo(IDiff diff, IResourceVariantComparator comparator) {
+ if (diff instanceof ResourceDiff) {
+ ResourceDiff rd = (ResourceDiff) diff;
+ IResource local = rd.getResource();
+ IFileRevision afterState = rd.getAfterState();
+ IResourceVariant remote = asResourceVariant(afterState);
+ int kind;
+ if (remote == null) {
+ kind = SyncInfo.DELETION;
+ } else if (!local.exists()) {
+ kind = SyncInfo.ADDITION;
+ } else {
+ kind = SyncInfo.CHANGE;
+ }
+ SyncInfo info = createSyncInfo(comparator, kind, local, null, remote);
+ return info;
+ } else if (diff instanceof IThreeWayDiff) {
+ IThreeWayDiff twd = (IThreeWayDiff) diff;
+ IResource local = getLocal(twd);
+ if (local != null) {
+ IResourceVariant remote = getRemoteVariant(twd);
+ IResourceVariant base = getBaseVariant(twd);
+ int kind = asSyncInfoKind(twd);
+ SyncInfo info = createSyncInfo(comparator, kind, local, base, remote);
+ return info;
+ }
+ }
+ return null;
+ }
+
+ protected SyncInfo createSyncInfo(IResourceVariantComparator comparator, int kind, IResource local, IResourceVariant base, IResourceVariant remote) {
+ PrecalculatedSyncInfo info = new PrecalculatedSyncInfo(kind, local, base, remote, comparator);
+ try {
+ info.init();
+ } catch (TeamException e) {
+ // Ignore
+ }
+ return info;
+ }
+
+ private static IResource getLocal(IThreeWayDiff twd) {
+ IResourceDiff diff = (IResourceDiff)twd.getRemoteChange();
+ if (diff != null)
+ return diff.getResource();
+ diff = (IResourceDiff)twd.getLocalChange();
+ if (diff != null)
+ return diff.getResource();
+ return null;
+ }
+
+ public static IResourceVariant asResourceVariant(IFileRevision revision) {
+ if (revision == null)
+ return null;
+ if (revision instanceof ResourceVariantFileRevision) {
+ ResourceVariantFileRevision rvfr = (ResourceVariantFileRevision) revision;
+ return rvfr.getVariant();
+ }
+ if (revision instanceof IAdaptable) {
+ IAdaptable adaptable = (IAdaptable) revision;
+ Object o = adaptable.getAdapter(IResourceVariant.class);
+ if (o instanceof IResourceVariant) {
+ return (IResourceVariant) o;
+ }
+ }
+ return null;
+ }
+
+ public static IFileRevision getRemote(IDiff diff) {
+ if (diff instanceof IResourceDiff) {
+ IResourceDiff rd = (IResourceDiff) diff;
+ return rd.getAfterState();
+ }
+ if (diff instanceof IThreeWayDiff) {
+ IThreeWayDiff twd = (IThreeWayDiff) diff;
+ return getRemote(twd);
+ }
+ return null;
+ }
+
+ public static IFileRevision getRemote(IThreeWayDiff twd) {
+ IResourceDiff rd = (IResourceDiff)twd.getRemoteChange();
+ if (rd != null)
+ return rd.getAfterState();
+ rd = (IResourceDiff)twd.getLocalChange();
+ if (rd != null)
+ return rd.getBeforeState();
+ return null;
+ }
+
+ public static SyncInfoToDiffConverter getDefault() {
+ if (instance == null)
+ instance = new SyncInfoToDiffConverter();
+ return instance;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/TextStorageMerger.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/TextStorageMerger.java
new file mode 100644
index 000000000..562fe55e2
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/mapping/TextStorageMerger.java
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.mapping;
+
+import java.io.*;
+
+import org.eclipse.compare.rangedifferencer.RangeDifference;
+import org.eclipse.compare.rangedifferencer.RangeDifferencer;
+import org.eclipse.core.resources.IStorage;
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.mapping.IStorageMerger;
+import org.eclipse.team.internal.core.Messages;
+import org.eclipse.team.internal.core.TeamPlugin;
+
+public class TextStorageMerger implements IStorageMerger {
+
+ @Override
+ public IStatus merge(OutputStream output, String outputEncoding,
+ IStorage ancestor, IStorage target, IStorage other,
+ IProgressMonitor monitor) throws CoreException {
+
+ LineComparator a, t, o;
+
+ try {
+ a= LineComparator.create(ancestor, outputEncoding);
+ t= LineComparator.create(target, outputEncoding);
+ o= LineComparator.create(other,outputEncoding);
+ } catch (UnsupportedEncodingException e) {
+ throw new CoreException (new Status(IStatus.ERROR, TeamPlugin.ID, UNSUPPORTED_ENCODING, Messages.TextAutoMerge_inputEncodingError, e));
+ } catch (IOException e) {
+ throw new CoreException (new Status(IStatus.ERROR, TeamPlugin.ID, INTERNAL_ERROR, e.getMessage(), e));
+ }
+
+ try {
+ boolean firstLine = true;
+ String lineSeparator= System.getProperty("line.separator"); //$NON-NLS-1$
+ if (lineSeparator == null)
+ lineSeparator= "\n"; //$NON-NLS-1$
+
+ RangeDifference[] diffs= RangeDifferencer.findRanges(monitor, a, t, o);
+
+ for (int i= 0; i < diffs.length; i++) {
+ RangeDifference rd= diffs[i];
+ switch (rd.kind()) {
+ case RangeDifference.ANCESTOR: // pseudo conflict
+ case RangeDifference.NOCHANGE:
+ case RangeDifference.RIGHT:
+ for (int j= rd.rightStart(); j < rd.rightEnd(); j++) {
+ String s= o.getLine(j);
+ if (!firstLine)
+ output.write(lineSeparator.getBytes(outputEncoding));
+ output.write(s.getBytes(outputEncoding));
+ firstLine = false;
+ }
+ break;
+
+ case RangeDifference.LEFT:
+ for (int j= rd.leftStart(); j < rd.leftEnd(); j++) {
+ String s= t.getLine(j);
+ if (!firstLine)
+ output.write(lineSeparator.getBytes(outputEncoding));
+ output.write(s.getBytes(outputEncoding));
+ firstLine = false;
+ }
+ break;
+
+ case RangeDifference.CONFLICT:
+ return new Status(IStatus.WARNING, TeamPlugin.ID, CONFLICT, Messages.TextAutoMerge_conflict, null);
+
+ default:
+ break;
+ }
+ }
+
+ } catch (UnsupportedEncodingException e) {
+ throw new CoreException (new Status(IStatus.ERROR, TeamPlugin.ID, UNSUPPORTED_ENCODING, Messages.TextAutoMerge_outputEncodingError, e));
+ } catch (IOException e) {
+ return new Status(IStatus.ERROR, TeamPlugin.ID, INTERNAL_ERROR, Messages.TextAutoMerge_outputIOError, e);
+ }
+
+ return Status.OK_STATUS;
+ }
+
+ @Override
+ public boolean canMergeWithoutAncestor() {
+ return false;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/messages.properties b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/messages.properties
new file mode 100644
index 000000000..96f06dc34
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/messages.properties
@@ -0,0 +1,110 @@
+###############################################################################
+# Copyright (c) 2000, 2012 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
+# Olexiy Buyanskyy <olexiyb@gmail.com> - Bug 76386 - [History View] CVS Resource History shows revisions from all branches
+###############################################################################
+ok=OK
+concatStrings={0} {1}
+
+Assert_assertionFailed=Assertion failed: {0}
+
+FileModificationValidator_someReadOnly=Some files are read-only.
+FileModificationValidator_fileIsReadOnly=File {0} is read-only.
+FileModificationValidator_editFailed=Files are read-only.
+
+RepositoryProvider_Error_removing_nature_from_project___1=Error removing nature from project:
+RepositoryProvider_couldNotInstantiateProvider=Could not instantiate provider {1} for project {0}.
+RepositoryProvider_No_Provider_Registered=No provider registered for {0}.
+RepositoryProvider_linkedResourcesExist=Project ''{0}'' contains linked resources but the''{1}'' repository provider does not supported them.
+RepositoryProvider_linkedURIsExist=Project ''{0}'' contains linked URIs but the''{1}'' repository provider does not supported them.
+RepositoryProvider_linkedResourcesNotSupported=Project ''{0}'' is mapped to repository type ''{1}'' which does not support linked URIs.
+RepositoryProvider_linkedURIsNotSupported=Project ''{0}'' is mapped to repository type ''{1}'' which does not support linked resources.
+RepositoryProvider_couldNotClearAfterError=A serious error has occurred trying to map project ''{0}'' to provider ''{1}''. Please restart Eclipse.
+RepositoryProvider_invalidClass=Class ''{1}'' registered for id ''{0}'' is not a subclass of RepositoryProvider.
+RepositoryProvider_toString={0}:{1}
+
+Team_readError=An error occurred reading the state file ''{0}''
+
+PollingInputStream_readTimeout=Timeout while reading from input stream
+PollingInputStream_closeTimeout=Timeout while closing input stream
+PollingOutputStream_writeTimeout=Timeout while writing to output stream
+PollingOutputStream_closeTimeout=Timeout while closing output stream
+TimeoutOutputStream_cannotWriteToStream=Cannot write to output stream
+
+RemoteSyncElement_delimit=[{0}]
+RemoteSyncElement_insync=in-sync
+RemoteSyncElement_conflicting=conflicting
+RemoteSyncElement_outgoing=outgoing
+RemoteSyncElement_incoming=incoming
+RemoteSyncElement_change=change
+RemoteSyncElement_addition=addition
+RemoteSyncElement_deletion=deletion
+RemoteSyncElement_manual={manual}
+RemoteSyncElement_auto={auto}
+
+Team_Error_loading_ignore_state_from_disk_1=Error loading ignore state from disk
+Team_Conflict_occured_for_ignored_resources_pattern=A conflict occured for Ignored Resources pattern: {0}, contributed by extensions: {1}. The pattern has been disabled. You can change the pattern enablement on Ignored Resources preference page.
+
+RemoteContentsCache_cacheDisposed=The cache for {0} is disposed.
+RemoteContentsCache_fileError=An I/O error performing an operation on {0}.
+
+SubscriberEventHandler_2=Updating {0}.
+SubscriberEventHandler_jobName=Updating Synchronize view for {0}.
+SubscriberChangeSetCollector_0=An error occurred while reconciling change sets. Restarting the application is recommended.
+SubscriberChangeSetCollector_1=Updating Change Sets for {0}
+SubscriberChangeSetCollector_2=Errors occurred while updating the change sets for {0}
+SubscriberChangeSetCollector_3=An error occurred saving the change set state for {0}
+SubscriberChangeSetCollector_4=An error occurred restoring the change set state for {0}
+SubscriberChangeSetCollector_5=An error occurred purging the commit set state for {0}
+SubscriberResourceMappingContext_0=Remote counterpart of {0} is a container
+SubscriberResourceMappingContext_1=Remote counterpart of {0} is not a container
+SubscriberDiffTreeEventHandler_0=Checking {0}
+SubscriberEventHandler_errors=Errors have occurred while calculating the synchronization state for {0}.
+RemoteContentsCacheEntry_3=Cache entry in {0} for {1} has been disposed
+SynchronizationCacheRefreshOperation_0=Processing {0}.
+SubscriberEventHandler_8=The members of folder {0} could not be retrieved: {1}
+SubscriberEventHandler_9=The synchronization state for resource {0} could not be determined: {1}
+SubscriberEventHandler_10=An internal error occurred processing subscriber events.
+SubscriberEventHandler_11=An internal error occurred processing resource {0}: {1}
+CachedResourceVariant_0=There is no cached contents for resource {0}.
+CachedResourceVariant_1=As error occurred computing the content type of resource variant {0}
+SyncInfoTree_0=Sync info is missing for resource {0}.
+ResourceVariantTreeSubscriber_1=Problems reported while synchronizing {0}. {1} of {2} resources were synchronized.
+ResourceVariantTreeSubscriber_2=An error occurred synchronizing {0}: {1}
+ResourceVariantTreeSubscriber_3=Problems reported while synchronizing {0}. {1} of {2} resources were synchronized, number of synchronizations canceled: {3}.
+ResourceVariantTreeSubscriber_4=Synchronization of {0} canceled because login was canceled.
+SyncByteConverter_1=Malformed sync byte format detected in {0}
+BatchingLock_11=An error occurred while flushing batched changes
+SubscriberEventHandler_12=Synchronization state collection canceled by a user action.
+ProjectSetCapability_0=Failed to create project references
+ProjectSetCapability_1=Failed to load projects
+AbstractResourceVariantTree_0=Processing {0}
+MergeContext_0=Some conflicting changes cannot be merged automatically. These changes will have to be merged manually.
+MergeContext_1=Conflicting change could not be merged: {0}
+MergeContext_2=Merge of {0} failed due to an internal error.
+MergeContext_3=Auto-merge support for {0} is not available.
+MergeContext_4=Could not read from temporary file {0}: {1}
+MergeContext_5=Merging {0}
+MergeContext_6=Updating {0}
+DelegatingStorageMerger_0=No storage merger could be found to merge the input
+DelegatingStorageMerger_1=An error occurred reading while reading from {0}
+
+LocalFileRevision_currentVersion=*({0})
+LocalFileRevision_currentVersionTag=<current version>
+LocalFileHistory_RefreshLocalHistory=Refreshing History for {0}
+LocalFileRevision_localRevisionTag=<local revision>
+WorkspaceSubscriber_0=Workspace
+WorkspaceSubscriber_1=Multiple errors occurred
+ScopeManagerEventHandler_0=Refreshing {0}
+ScopeManagerEventHandler_1=Errors occurred while refreshing {0}
+
+TextAutoMerge_inputEncodingError= Unsupported encoding for input stream
+TextAutoMerge_outputEncodingError= Unsupported encoding for output stream
+TextAutoMerge_outputIOError= I/O error on writing
+TextAutoMerge_conflict= Conflict: cannot auto-merge
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/CRLFtoLFInputStream.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/CRLFtoLFInputStream.java
new file mode 100644
index 000000000..2621578fd
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/CRLFtoLFInputStream.java
@@ -0,0 +1,160 @@
+/*******************************************************************************
+ * 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.internal.core.streams;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+
+/**
+ * Converts CR/LFs in the underlying input stream to LF.
+ *
+ * Supports resuming partially completed operations after an InterruptedIOException
+ * if the underlying stream does. Check the bytesTransferred field to determine how
+ * much of the operation completed; conversely, at what point to resume.
+ */
+public class CRLFtoLFInputStream extends FilterInputStream {
+ private boolean pendingByte = false;
+ private int lastByte = -1;
+
+ /**
+ * Creates a new filtered input stream.
+ * @param in the underlying input stream
+ */
+ public CRLFtoLFInputStream(InputStream in) {
+ super(in);
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Translates CR/LF sequences to LFs transparently.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read() throws IOException {
+ if (! pendingByte) {
+ lastByte = in.read(); // ok if this throws
+ pendingByte = true; // remember the byte in case we throw an exception later on
+ }
+ if (lastByte == '\r') {
+ lastByte = in.read(); // ok if this throws
+ if (lastByte != '\n') {
+ if (lastByte == -1) pendingByte = false;
+ return '\r'; // leaves the byte pending for later
+ }
+ }
+ pendingByte = false;
+ return lastByte;
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Translates CR/LF sequences to LFs transparently.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read(byte[] buffer, int off, int len) throws IOException {
+ // handle boundary cases cleanly
+ if (len == 0) {
+ return 0;
+ } else if (len == 1) {
+ int b = read();
+ if (b == -1) return -1;
+ buffer[off] = (byte) b;
+ return 1;
+ }
+ // read some bytes from the stream
+ // prefix with pending byte from last read if any
+ int count = 0;
+ if (pendingByte) {
+ buffer[off] = (byte) lastByte;
+ pendingByte = false;
+ count = 1;
+ }
+ InterruptedIOException iioe = null;
+ try {
+ len = in.read(buffer, off + count, len - count);
+ if (len == -1) {
+ return (count == 0) ? -1 : count;
+ }
+ } catch (InterruptedIOException e) {
+ len = e.bytesTransferred;
+ iioe = e;
+ }
+ count += len;
+ // strip out CR's in CR/LF pairs
+ // pendingByte will be true iff previous byte was a CR
+ int j = off;
+ for (int i = off; i < off + count; ++i) { // invariant: j <= i
+ lastByte = buffer[i];
+ if (lastByte == '\r') {
+ if (pendingByte) {
+ buffer[j++] = '\r'; // write out orphan CR
+ } else {
+ pendingByte = true;
+ }
+ } else {
+ if (pendingByte) {
+ if (lastByte != '\n') buffer[j++] = '\r'; // if LF, don't write the CR
+ pendingByte = false;
+ }
+ buffer[j++] = (byte) lastByte;
+ }
+ }
+ if (iioe != null) {
+ iioe.bytesTransferred = j - off;
+ throw iioe;
+ }
+ return j - off;
+ }
+
+ /**
+ * Calls read() to skip the specified number of bytes
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public long skip(long count) throws IOException {
+ int actualCount = 0; // assumes count < Integer.MAX_INT
+ try {
+ while (count-- > 0 && read() != -1) actualCount++; // skip the specified number of bytes
+ return actualCount;
+ } catch (InterruptedIOException e) {
+ e.bytesTransferred = actualCount;
+ throw e;
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Returns the number of bytes that can be read without blocking; accounts for
+ * possible translation of CR/LF sequences to LFs in these bytes.
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int available() throws IOException {
+ return in.available() / 2; // we can guarantee at least this amount after contraction
+ }
+
+ /**
+ * Mark is not supported by the wrapper even if the underlying stream does, returns false.
+ */
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/LFtoCRLFInputStream.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/LFtoCRLFInputStream.java
new file mode 100644
index 000000000..20c608db0
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/LFtoCRLFInputStream.java
@@ -0,0 +1,151 @@
+/*******************************************************************************
+ * 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.internal.core.streams;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+
+/**
+ * Converts LFs in the underlying input stream to CR/LF.
+ *
+ * Supports resuming partially completed operations after an InterruptedIOException
+ * if the underlying stream does. Check the bytesTransferred field to determine how
+ * much of the operation completed; conversely, at what point to resume.
+ */
+public class LFtoCRLFInputStream extends FilterInputStream {
+ private boolean mustReturnLF = false;
+
+ /**
+ * Creates a new filtered input stream.
+ * @param in the underlying input stream
+ */
+ public LFtoCRLFInputStream(InputStream in) {
+ super(in);
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Translates LFs to CR/LF sequences transparently.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read() throws IOException {
+ if (mustReturnLF) {
+ mustReturnLF = false;
+ return '\n';
+ }
+ int b = in.read(); // ok if this throws
+ if (b == '\n') {
+ mustReturnLF = true;
+ b = '\r';
+ }
+ return b;
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Translates LFs to CR/LF sequences transparently.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read(byte[] buffer, int off, int len) throws IOException {
+ // handle boundary cases cleanly
+ if (len == 0) {
+ return 0;
+ } else if (len == 1) {
+ int b = read();
+ if (b == -1) return -1;
+ buffer[off] = (byte) b;
+ return 1;
+ }
+ // prefix with remembered \n from last read, but don't expand it a second time
+ int count = 0;
+ if (mustReturnLF) {
+ mustReturnLF = false;
+ buffer[off++] = '\n';
+ --len;
+ count = 1;
+ if (len < 2) return count; // is there still enough room to expand more?
+ }
+ // read some bytes from the stream into the back half of the buffer
+ // this guarantees that there is always room to expand
+ len /= 2;
+ int j = off + len;
+ InterruptedIOException iioe = null;
+ try {
+ len = in.read(buffer, j, len);
+ if (len == -1) {
+ return (count == 0) ? -1 : count;
+ }
+ } catch (InterruptedIOException e) {
+ len = e.bytesTransferred;
+ iioe = e;
+ }
+ count += len;
+ // copy bytes from the middle to the front of the array, expanding LF->CR/LF
+ while (len-- > 0) {
+ byte b = buffer[j++];
+ if (b == '\n') {
+ buffer[off++] = '\r';
+ count++;
+ }
+ buffer[off++] = b;
+ }
+ if (iioe != null) {
+ iioe.bytesTransferred = count;
+ throw iioe;
+ }
+ return count;
+ }
+
+ /**
+ * Calls read() to skip the specified number of bytes
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public long skip(long count) throws IOException {
+ int actualCount = 0; // assumes count < Integer.MAX_INT
+ try {
+ while (count-- > 0 && read() != -1) actualCount++; // skip the specified number of bytes
+ return actualCount;
+ } catch (InterruptedIOException e) {
+ e.bytesTransferred = actualCount;
+ throw e;
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Returns the number of bytes that can be read without blocking; accounts for
+ * possible translation of LFs to CR/LF sequences in these bytes.
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int available() throws IOException {
+ return in.available(); // we can guarantee at least this amount after expansion
+ }
+
+ /**
+ * Mark is not supported by the wrapper even if the underlying stream does, returns false.
+ */
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/PollingInputStream.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/PollingInputStream.java
new file mode 100644
index 000000000..f612fc49d
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/PollingInputStream.java
@@ -0,0 +1,202 @@
+/*******************************************************************************
+ * 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.internal.core.streams;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.team.internal.core.*;
+import org.eclipse.team.internal.core.Policy;
+import org.eclipse.team.internal.core.TeamPlugin;
+
+/**
+ * Polls a progress monitor periodically and handles timeouts over extended durations.
+ * For this class to be effective, a high numAttempts should be specified, and the
+ * underlying stream should time out frequently on reads (every second or so).
+ *
+ * Supports resuming partially completed operations after an InterruptedIOException
+ * if the underlying stream does. Check the bytesTransferred field to determine how
+ * much of the operation completed; conversely, at what point to resume.
+ */
+public class PollingInputStream extends FilterInputStream {
+ private int numAttempts;
+ private IProgressMonitor monitor;
+ private boolean cancellable;
+
+ /**
+ * Creates a new polling input stream.
+ * @param in the underlying input stream
+ * @param numAttempts the number of attempts before issuing an InterruptedIOException,
+ * if 0, retries indefinitely until canceled
+ * @param monitor the progress monitor to be polled for cancellation
+ */
+ public PollingInputStream(InputStream in, int numAttempts, IProgressMonitor monitor) {
+ super(in);
+ this.numAttempts = numAttempts;
+ this.monitor = monitor;
+ this.cancellable = true;
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * It may be important to wait for an input stream to be closed because it
+ * holds an implicit lock on a system resource (such as a file) while it is
+ * open. Closing a stream may take time if the underlying stream is still
+ * servicing a previous request.
+ * @throws OperationCanceledException if the progress monitor is canceled
+ * @throws InterruptedIOException if the underlying operation times out numAttempts times
+ */
+ @Override
+ public void close() throws InterruptedIOException {
+ int attempts = 0;
+ try {
+ readPendingInput();
+ } catch (IOException e) {
+ // We shouldn't get an exception when we're getting the available input.
+ // If we do, just log it so we can close.
+ TeamPlugin.log(IStatus.ERROR, e.getMessage(), e);
+ } finally {
+ boolean stop = false;
+ while (!stop) {
+ try {
+ if (in != null)
+ in.close();
+ stop = true;
+ } catch (InterruptedIOException e) {
+ if (checkCancellation()) throw new OperationCanceledException();
+ if (++attempts == numAttempts)
+ throw new InterruptedIOException(Messages.PollingInputStream_closeTimeout);
+ if (Policy.DEBUG_STREAMS) System.out.println("close retry=" + attempts); //$NON-NLS-1$
+ } catch (IOException e) {
+ // ignore it - see https://bugs.eclipse.org/bugs/show_bug.cgi?id=203423#c10
+ }
+ }
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * @return the next byte of data, or -1 if the end of the stream is reached.
+ * @throws OperationCanceledException if the progress monitor is canceled
+ * @throws InterruptedIOException if the underlying operation times out numAttempts times
+ * and no data was received, bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read() throws IOException {
+ int attempts = 0;
+ for (;;) {
+ if (checkCancellation()) throw new OperationCanceledException();
+ try {
+ return in.read();
+ } catch (InterruptedIOException e) {
+ if (++attempts == numAttempts)
+ throw new InterruptedIOException(Messages.PollingInputStream_readTimeout);
+ if (Policy.DEBUG_STREAMS) System.out.println("read retry=" + attempts); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * @param buffer - the buffer into which the data is read.
+ * @param off - the start offset of the data.
+ * @param len - the maximum number of bytes read.
+ * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the stream has been reached.
+ * @throws OperationCanceledException if the progress monitor is canceled
+ * @throws InterruptedIOException if the underlying operation times out numAttempts times
+ * and no data was received, bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read(byte[] buffer, int off, int len) throws IOException {
+ int attempts = 0;
+ for (;;) {
+ if (checkCancellation()) throw new OperationCanceledException();
+ try {
+ return in.read(buffer, off, len);
+ } catch (InterruptedIOException e) {
+ if (e.bytesTransferred != 0) return e.bytesTransferred; // keep partial transfer
+ if (++attempts == numAttempts)
+ throw new InterruptedIOException(Messages.PollingInputStream_readTimeout);
+ if (Policy.DEBUG_STREAMS) System.out.println("read retry=" + attempts); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * @param count - the number of bytes to be skipped.
+ * @return the actual number of bytes skipped.
+ * @throws OperationCanceledException if the progress monitor is canceled
+ * @throws InterruptedIOException if the underlying operation times out numAttempts times
+ * and no data was received, bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public long skip(long count) throws IOException {
+ int attempts = 0;
+ for (;;) {
+ if (checkCancellation()) throw new OperationCanceledException();
+ try {
+ return in.skip(count);
+ } catch (InterruptedIOException e) {
+ if (e.bytesTransferred != 0) return e.bytesTransferred; // keep partial transfer
+ if (++attempts == numAttempts)
+ throw new InterruptedIOException(Messages.PollingInputStream_readTimeout);
+ if (Policy.DEBUG_STREAMS) System.out.println("read retry=" + attempts); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * Reads any pending input from the input stream so that
+ * the stream can savely be closed.
+ */
+ protected void readPendingInput() throws IOException {
+ byte[] buffer= new byte[2048];
+ while (true) {
+ int available = in.available();
+ if (available < 1) break;
+ if (available > buffer.length) available = buffer.length;
+ if (in.read(buffer, 0, available) < 1) break;
+ }
+ }
+
+ /**
+ * Called to set whether cancellation will be checked by this stream. Turning cancellation checking
+ * off can be very useful for protecting critical portions of a protocol that shouldn't be interrupted.
+ * For example, it is often necessary to protect login sequences.
+ * @param cancellable a flag controlling whether this stream will check for cancellation.
+ */
+ public void setIsCancellable(boolean cancellable) {
+ this.cancellable = cancellable;
+ }
+
+ /**
+ * Checked whether the monitor for this stream has been cancelled. If the cancellable
+ * flag is <code>false</code> then the monitor is never cancelled.
+ * @return <code>true</code> if the monitor has been cancelled and <code>false</code>
+ * otherwise.
+ */
+ private boolean checkCancellation() {
+ if(cancellable) {
+ return monitor.isCanceled();
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/PollingOutputStream.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/PollingOutputStream.java
new file mode 100644
index 000000000..3ad9d061b
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/PollingOutputStream.java
@@ -0,0 +1,193 @@
+/*******************************************************************************
+ * 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.internal.core.streams;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.team.internal.core.Messages;
+import org.eclipse.team.internal.core.Policy;
+
+/**
+ * Polls a progress monitor periodically and handles timeouts over extended durations.
+ * For this class to be effective, a high numAttempts should be specified, and the
+ * underlying stream should time out frequently on writes (every second or so).
+ *
+ * Supports resuming partially completed operations after an InterruptedIOException
+ * if the underlying stream does. Check the bytesTransferred field to determine how
+ * much of the operation completed; conversely, at what point to resume.
+ */
+public class PollingOutputStream extends FilterOutputStream {
+ private int numAttempts;
+ private IProgressMonitor monitor;
+ private boolean cancellable;
+
+ /**
+ * Creates a new polling output stream.
+ * @param out the underlying output stream
+ * @param numAttempts the number of attempts before issuing an InterruptedIOException,
+ * if 0, retries indefinitely until canceled
+ * @param monitor the progress monitor to be polled for cancellation
+ */
+ public PollingOutputStream(OutputStream out, int numAttempts, IProgressMonitor monitor) {
+ super(out);
+ this.numAttempts = numAttempts;
+ this.monitor = monitor;
+ this.cancellable = true;
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * @throws OperationCanceledException if the progress monitor is canceled
+ * @throws InterruptedIOException if the underlying operation times out numAttempts times
+ * and no data was sent, bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public void write(int b) throws IOException {
+ int attempts = 0;
+ for (;;) {
+ if (checkCancellation()) throw new OperationCanceledException();
+ try {
+ out.write(b);
+ return;
+ } catch (InterruptedIOException e) {
+ if (++attempts == numAttempts)
+ throw new InterruptedIOException(Messages.PollingOutputStream_writeTimeout);
+ if (Policy.DEBUG_STREAMS) System.out.println("write retry=" + attempts); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * @throws OperationCanceledException if the progress monitor is canceled
+ * @throws InterruptedIOException if the underlying operation times out numAttempts times,
+ * bytesTransferred will reflect the number of bytes sent
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public void write(byte[] buffer, int off, int len) throws IOException {
+ int count = 0;
+ int attempts = 0;
+ for (;;) {
+ if (checkCancellation()) throw new OperationCanceledException();
+ try {
+ out.write(buffer, off, len);
+ return;
+ } catch (InterruptedIOException e) {
+ int amount = e.bytesTransferred;
+ if (amount != 0) { // keep partial transfer
+ len -= amount;
+ if (len <= 0) return;
+ off += amount;
+ count += amount;
+ attempts = 0; // made some progress, don't time out quite yet
+ }
+ if (++attempts == numAttempts) {
+ e = new InterruptedIOException(Messages.PollingOutputStream_writeTimeout);
+ e.bytesTransferred = count;
+ throw e;
+ }
+ if (Policy.DEBUG_STREAMS) System.out.println("write retry=" + attempts); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * @throws OperationCanceledException if the progress monitor is canceled
+ * @throws InterruptedIOException if the underlying operation times out numAttempts times,
+ * bytesTransferred will reflect the number of bytes sent
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public void flush() throws IOException {
+ int count = 0;
+ int attempts = 0;
+ for (;;) {
+ if (checkCancellation()) throw new OperationCanceledException();
+ try {
+ out.flush();
+ return;
+ } catch (InterruptedIOException e) {
+ int amount = e.bytesTransferred;
+ if (amount != 0) { // keep partial transfer
+ count += amount;
+ attempts = 0; // made some progress, don't time out quite yet
+ }
+ if (++attempts == numAttempts) {
+ e = new InterruptedIOException(Messages.PollingOutputStream_writeTimeout);
+ e.bytesTransferred = count;
+ throw e;
+ }
+ if (Policy.DEBUG_STREAMS) System.out.println("write retry=" + attempts); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * Calls flush() then close() on the underlying stream.
+ * @throws OperationCanceledException if the progress monitor is canceled
+ * @throws InterruptedIOException if the underlying operation times out numAttempts times,
+ * bytesTransferred will reflect the number of bytes sent during the flush()
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public void close() throws IOException {
+ int attempts = numAttempts - 1; // fail fast if flush() does times out
+ try {
+ out.flush();
+ attempts = 0;
+ } finally {
+ boolean stop = false;
+ while (!stop) {
+ try {
+ out.close();
+ stop = true;
+ } catch (InterruptedIOException e) {
+ if (checkCancellation()) throw new OperationCanceledException();
+ if (++attempts == numAttempts)
+ throw new InterruptedIOException(Messages.PollingOutputStream_closeTimeout);
+ if (Policy.DEBUG_STREAMS) System.out.println("close retry=" + attempts); //$NON-NLS-1$
+ }
+ }
+ }
+ }
+
+ /**
+ * Called to set whether cancellation will be checked by this stream. Turning cancellation checking
+ * off can be very useful for protecting critical portions of a protocol that shouldn't be interrupted.
+ * For example, it is often necessary to protect login sequences.
+ * @param cancellable a flag controlling whether this stream will check for cancellation.
+ */
+ public void setIsCancellable(boolean cancellable) {
+ this.cancellable = cancellable;
+ }
+
+ /**
+ * Checked whether the monitor for this stream has been cancelled. If the cancellable
+ * flag is <code>false</code> then the monitor is never cancelled.
+ * @return <code>true</code> if the monitor has been cancelled and <code>false</code>
+ * otherwise.
+ */
+ private boolean checkCancellation() {
+ if(cancellable) {
+ return monitor.isCanceled();
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/ProgressMonitorInputStream.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/ProgressMonitorInputStream.java
new file mode 100644
index 000000000..d4a85fc0d
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/ProgressMonitorInputStream.java
@@ -0,0 +1,144 @@
+/*******************************************************************************
+ * 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.internal.core.streams;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+
+/**
+ * Updates a progress monitor as bytes are read from the input stream.
+ * Also starts a background thread to provide responsive cancellation on read().
+ *
+ * Supports resuming partially completed operations after an InterruptedIOException
+ * if the underlying stream does. Check the bytesTransferred field to determine how
+ * much of the operation completed; conversely, at what point to resume.
+ */
+public abstract class ProgressMonitorInputStream extends FilterInputStream {
+ private IProgressMonitor monitor;
+ private int updateIncrement;
+ private long bytesTotal;
+ private long bytesRead = 0;
+ private long lastUpdate = -1;
+ private long nextUpdate = 0;
+
+ /**
+ * Creates a progress monitoring input stream.
+ * @param in the underlying input stream
+ * @param bytesTotal the number of bytes to read in total (passed to updateMonitor())
+ * @param updateIncrement the number of bytes read between updates
+ * @param monitor the progress monitor
+ */
+ public ProgressMonitorInputStream(InputStream in, long bytesTotal, int updateIncrement, IProgressMonitor monitor) {
+ super(in);
+ this.bytesTotal = bytesTotal;
+ this.updateIncrement = updateIncrement;
+ this.monitor = monitor;
+ update(true);
+ }
+
+ protected abstract void updateMonitor(long bytesRead, long size, IProgressMonitor monitor);
+
+ /**
+ * Wraps the underlying stream's method.
+ * Updates the progress monitor to the final number of bytes read.
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public void close() throws IOException {
+ try {
+ in.close();
+ } finally {
+ update(true);
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Updates the progress monitor if the next update increment has been reached.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read() throws IOException {
+ int b = in.read();
+ if (b != -1) {
+ bytesRead += 1;
+ update(false);
+ }
+ return b;
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Updates the progress monitor if the next update increment has been reached.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ try {
+ int count = in.read(buffer, offset, length);
+ if (count != -1) {
+ bytesRead += count;
+ update(false);
+ }
+ return count;
+ } catch (InterruptedIOException e) {
+ bytesRead += e.bytesTransferred;
+ update(false);
+ throw e;
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Updates the progress monitor if the next update increment has been reached.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public long skip(long amount) throws IOException {
+ try {
+ long count = in.skip(amount);
+ bytesRead += count;
+ update(false);
+ return count;
+ } catch (InterruptedIOException e) {
+ bytesRead += e.bytesTransferred;
+ update(false);
+ throw e;
+ }
+ }
+
+ /**
+ * Mark is not supported by the wrapper even if the underlying stream does, returns false.
+ */
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ private void update(boolean now) {
+ if (bytesRead >= nextUpdate || now) {
+ nextUpdate = bytesRead - (bytesRead % updateIncrement);
+ if (nextUpdate != lastUpdate) updateMonitor(nextUpdate, bytesTotal, monitor);
+ lastUpdate = nextUpdate;
+ nextUpdate += updateIncrement;
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/SizeConstrainedInputStream.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/SizeConstrainedInputStream.java
new file mode 100644
index 000000000..7919f8208
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/SizeConstrainedInputStream.java
@@ -0,0 +1,144 @@
+/*******************************************************************************
+ * 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.internal.core.streams;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+
+import org.eclipse.core.runtime.OperationCanceledException;
+
+/**
+ * Simulates a stream that represents only a portion of the underlying stream.
+ * Will report EOF when this portion has been fully read and prevent further reads.
+ * The underlying stream is not closed on close(), but the remaining unread input
+ * may optionally be skip()'d.
+ *
+ * Supports resuming partially completed operations after an InterruptedIOException
+ * if the underlying stream does. Check the bytesTransferred field to determine how
+ * much of the operation completed; conversely, at what point to resume.
+ */
+public class SizeConstrainedInputStream extends FilterInputStream {
+ private boolean discardOnClose;
+ private long bytesRemaining;
+
+ /**
+ * Creates a size constrained input stream.
+ * @param in the underlying input stream, never actually closed by this filter
+ * @param size the maximum number of bytes of the underlying input stream that
+ * can be read through this filter
+ * @param discardOnClose if true, discards remaining unread bytes on close()
+ */
+ public SizeConstrainedInputStream(InputStream in, long size, boolean discardOnClose) {
+ super(in);
+ this.bytesRemaining = size;
+ this.discardOnClose = discardOnClose;
+ }
+
+ /**
+ * Prevents further reading from the stream but does not close the underlying stream.
+ * If discardOnClose, skip()'s over any remaining unread bytes in the constrained region.
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public void close() throws IOException {
+ try {
+ if (discardOnClose) {
+ while (bytesRemaining != 0 && skip(bytesRemaining) != 0);
+ }
+ } catch (OperationCanceledException e) {
+ // The receiver is likely wrapping a PollingInputStream which could throw
+ // an OperationCanceledException on a skip.
+ // Since we're closing, just ignore the cancel and let the caller check the monitor
+ } finally {
+ bytesRemaining = 0;
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Simulates an end-of-file condition if the end of the constrained region has been reached.
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int available() throws IOException {
+ int amount = in.available();
+ if (amount > bytesRemaining) amount = (int) bytesRemaining;
+ return amount;
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Simulates an end-of-file condition if the end of the constrained region has been reached.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read() throws IOException {
+ if (bytesRemaining == 0) return -1;
+ int b = in.read();
+ if (b != -1) bytesRemaining -= 1;
+ return b;
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Simulates an end-of-file condition if the end of the constrained region has been reached.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ if (length > bytesRemaining) {
+ if (bytesRemaining == 0) return -1;
+ length = (int) bytesRemaining;
+ }
+ try {
+ int count = in.read(buffer, offset, length);
+ if (count != -1) bytesRemaining -= count;
+ return count;
+ } catch (InterruptedIOException e) {
+ bytesRemaining -= e.bytesTransferred;
+ throw e;
+ }
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * Simulates an end-of-file condition if the end of the constrained region has been reached.
+ * @throws InterruptedIOException if the operation was interrupted before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public long skip(long amount) throws IOException {
+ if (amount > bytesRemaining) amount = bytesRemaining;
+ try {
+ long count = in.skip(amount);
+ bytesRemaining -= count;
+ return count;
+ } catch (InterruptedIOException e) {
+ bytesRemaining -= e.bytesTransferred;
+ throw e;
+ }
+ }
+
+ /**
+ * Mark is not supported by the wrapper even if the underlying stream does, returns false.
+ */
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/TimeoutInputStream.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/TimeoutInputStream.java
new file mode 100644
index 000000000..66961a544
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/TimeoutInputStream.java
@@ -0,0 +1,332 @@
+/*******************************************************************************
+ * 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.internal.core.streams;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import org.eclipse.team.internal.core.Policy;
+
+/**
+ * Wraps an input stream that blocks indefinitely to simulate timeouts on read(),
+ * skip(), and close(). The resulting input stream is buffered and supports
+ * retrying operations that failed due to an InterruptedIOException.
+ *
+ * Supports resuming partially completed operations after an InterruptedIOException
+ * REGARDLESS of whether the underlying stream does unless the underlying stream itself
+ * generates InterruptedIOExceptions in which case it must also support resuming.
+ * Check the bytesTransferred field to determine how much of the operation completed;
+ * conversely, at what point to resume.
+ */
+public class TimeoutInputStream extends FilterInputStream {
+ // unsynchronized variables
+ private final long readTimeout; // read() timeout in millis
+ private final long closeTimeout; // close() timeout in millis, or -1
+
+ // requests for the thread (synchronized)
+ private boolean closeRequested = false; // if true, close requested
+
+ // responses from the thread (synchronized)
+ private Thread thread; // if null, thread has terminated
+ private byte[] iobuffer; // circular buffer
+ private int head = 0; // points to first unread byte
+ private int length = 0; // number of remaining unread bytes
+ private IOException ioe = null; // if non-null, contains a pending exception
+ private boolean waitingForClose = false; // if true, thread is waiting for close()
+
+ private boolean growWhenFull = false; // if true, buffer will grow when it is full
+
+ /**
+ * Creates a timeout wrapper for an input stream.
+ * @param in the underlying input stream
+ * @param bufferSize the buffer size in bytes; should be large enough to mitigate
+ * Thread synchronization and context switching overhead
+ * @param readTimeout the number of milliseconds to block for a read() or skip() before
+ * throwing an InterruptedIOException; 0 blocks indefinitely
+ * @param closeTimeout the number of milliseconds to block for a close() before throwing
+ * an InterruptedIOException; 0 blocks indefinitely, -1 closes the stream in the background
+ */
+ public TimeoutInputStream(InputStream in, int bufferSize, long readTimeout, long closeTimeout) {
+ super(in);
+ this.readTimeout = readTimeout;
+ this.closeTimeout = closeTimeout;
+ this.iobuffer = new byte[bufferSize];
+ thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ runThread();
+ }
+ }, "TimeoutInputStream");//$NON-NLS-1$
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ public TimeoutInputStream(InputStream in, int bufferSize, long readTimeout, long closeTimeout, boolean growWhenFull) {
+ this(in, bufferSize, readTimeout, closeTimeout);
+ this.growWhenFull = growWhenFull;
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * It may be important to wait for a stream to actually be closed because it
+ * holds an implicit lock on a system resoure (such as a file) while it is
+ * open. Closing a stream may take time if the underlying stream is still
+ * servicing a previous request.
+ * @throws InterruptedIOException if the timeout expired
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public void close() throws IOException {
+ Thread oldThread;
+ synchronized (this) {
+ if (thread == null) return;
+ oldThread = thread;
+ closeRequested = true;
+ thread.interrupt();
+ checkError();
+ }
+ if (closeTimeout == -1) return;
+ try {
+ oldThread.join(closeTimeout);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // we weren't expecting to be interrupted
+ }
+ synchronized (this) {
+ checkError();
+ if (thread != null) throw new InterruptedIOException();
+ }
+ }
+
+ /**
+ * Returns the number of unread bytes in the buffer.
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public synchronized int available() throws IOException {
+ if (length == 0) checkError();
+ return length > 0 ? length : 0;
+ }
+
+ /**
+ * Reads a byte from the stream.
+ * @throws InterruptedIOException if the timeout expired and no data was received,
+ * bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public synchronized int read() throws IOException {
+ if (! syncFill()) return -1; // EOF reached
+ int b = iobuffer[head++] & 255;
+ if (head == iobuffer.length) head = 0;
+ length--;
+ notify();
+ return b;
+ }
+
+ /**
+ * Reads multiple bytes from the stream.
+ * @throws InterruptedIOException if the timeout expired and no data was received,
+ * bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public synchronized int read(byte[] buffer, int off, int len) throws IOException {
+ if (! syncFill()) return -1; // EOF reached
+ int pos = off;
+ if (len > length) len = length;
+ while (len-- > 0) {
+ buffer[pos++] = iobuffer[head++];
+ if (head == iobuffer.length) head = 0;
+ length--;
+ }
+ notify();
+ return pos - off;
+ }
+
+ /**
+ * Skips multiple bytes in the stream.
+ * @throws InterruptedIOException if the timeout expired before all of the
+ * bytes specified have been skipped, bytesTransferred may be non-zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public synchronized long skip(long count) throws IOException {
+ long amount = 0;
+ try {
+ do {
+ if (! syncFill()) break; // EOF reached
+ int skip = (int) Math.min(count - amount, length);
+ head = (head + skip) % iobuffer.length;
+ length -= skip;
+ amount += skip;
+ } while (amount < count);
+ } catch (InterruptedIOException e) {
+ e.bytesTransferred = (int) amount; // assumes amount < Integer.MAX_INT
+ throw e;
+ }
+ notify();
+ return amount;
+ }
+
+ /**
+ * Mark is not supported by the wrapper even if the underlying stream does, returns false.
+ */
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ /**
+ * Waits for the buffer to fill if it is empty and the stream has not reached EOF.
+ * @return true if bytes are available, false if EOF has been reached
+ * @throws InterruptedIOException if EOF not reached but no bytes are available
+ */
+ private boolean syncFill() throws IOException {
+ if (length != 0) return true;
+ checkError(); // check errors only after we have read all remaining bytes
+ if (waitingForClose) return false;
+ notify();
+ try {
+ wait(readTimeout);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // we weren't expecting to be interrupted
+ }
+ if (length != 0) return true;
+ checkError(); // check errors only after we have read all remaining bytes
+ if (waitingForClose) return false;
+ throw new InterruptedIOException();
+ }
+
+ /**
+ * If an exception is pending, throws it.
+ */
+ private void checkError() throws IOException {
+ if (ioe != null) {
+ IOException e = ioe;
+ ioe = null;
+ throw e;
+ }
+ }
+
+ /**
+ * Runs the thread in the background.
+ */
+ private void runThread() {
+ try {
+ readUntilDone();
+ } catch (IOException e) {
+ synchronized (this) { ioe = e; }
+ } finally {
+ waitUntilClosed();
+ try {
+ in.close();
+ } catch (IOException e) {
+ synchronized (this) { ioe = e; }
+ } finally {
+ synchronized (this) {
+ thread = null;
+ notify();
+ }
+ }
+ }
+ }
+
+ /**
+ * Waits until we have been requested to close the stream.
+ */
+ private synchronized void waitUntilClosed() {
+ waitingForClose = true;
+ notify();
+ while (! closeRequested) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ closeRequested = true; // alternate quit signal
+ }
+ }
+ }
+
+ /**
+ * Reads bytes into the buffer until EOF, closed, or error.
+ */
+ private void readUntilDone() throws IOException {
+ for (;;) {
+ int off, len;
+ synchronized (this) {
+ while (isBufferFull()) {
+ if (closeRequested) return; // quit signal
+ waitForRead();
+ }
+ off = (head + length) % iobuffer.length;
+ len = ((head > off) ? head : iobuffer.length) - off;
+ }
+ int count;
+ try {
+ // the i/o operation might block without releasing the lock,
+ // so we do this outside of the synchronized block
+ count = in.read(iobuffer, off, len);
+ if (count == -1) return; // EOF encountered
+ } catch (InterruptedIOException e) {
+ count = e.bytesTransferred; // keep partial transfer
+ }
+ synchronized (this) {
+ length += count;
+ notify();
+ }
+ }
+ }
+
+ /*
+ * Wait for a read when the buffer is full (with the implication
+ * that space will become available in the buffer after the read
+ * takes place).
+ */
+ private synchronized void waitForRead() {
+ try {
+ if (growWhenFull) {
+ // wait a second before growing to let reads catch up
+ wait(readTimeout);
+ } else {
+ wait();
+ }
+ } catch (InterruptedException e) {
+ closeRequested = true; // alternate quit signal
+ }
+ // If the buffer is still full, give it a chance to grow
+ if (growWhenFull && isBufferFull()) {
+ growBuffer();
+ }
+ }
+
+ private synchronized void growBuffer() {
+ int newSize = 2 * iobuffer.length;
+ if (newSize > iobuffer.length) {
+ if (Policy.DEBUG_STREAMS) {
+ System.out.println("InputStream growing to " + newSize + " bytes"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ byte[] newBuffer = new byte[newSize];
+ int pos = 0;
+ int len = length;
+ while (len-- > 0) {
+ newBuffer[pos++] = iobuffer[head++];
+ if (head == iobuffer.length) head = 0;
+ }
+ iobuffer = newBuffer;
+ head = 0;
+ // length instance variable was not changed by this method
+ }
+ }
+
+ private boolean isBufferFull() {
+ return length == iobuffer.length;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/TimeoutOutputStream.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/TimeoutOutputStream.java
new file mode 100644
index 000000000..deaa22a47
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/streams/TimeoutOutputStream.java
@@ -0,0 +1,289 @@
+/*******************************************************************************
+ * 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.internal.core.streams;
+
+import java.io.BufferedOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+
+import org.eclipse.team.internal.core.Messages;
+
+/**
+ * Wraps an output stream that blocks indefinitely to simulate timeouts on write(),
+ * flush(), and close(). The resulting output stream is buffered and supports
+ * retrying operations that failed due to an InterruptedIOException.
+ *
+ * Supports resuming partially completed operations after an InterruptedIOException
+ * REGARDLESS of whether the underlying stream does unless the underlying stream itself
+ * generates InterruptedIOExceptions in which case it must also support resuming.
+ * Check the bytesTransferred field to determine how much of the operation completed;
+ * conversely, at what point to resume.
+ */
+public class TimeoutOutputStream extends FilterOutputStream {
+ // unsynchronized variables
+ private final long writeTimeout; // write() timeout in millis
+ private final long closeTimeout; // close() timeout in millis, or -1
+
+ // requests for the thread (synchronized)
+ private byte[] iobuffer; // circular buffer
+ private int head = 0; // points to first unwritten byte
+ private int length = 0; // number of remaining unwritten bytes
+ private boolean closeRequested = false; // if true, close requested
+ private boolean flushRequested = false; // if true, flush requested
+
+ // responses from the thread (synchronized)
+ private Thread thread;
+ private boolean waitingForClose = false; // if true, the thread is waiting for close()
+ private IOException ioe = null;
+
+ /**
+ * Creates a timeout wrapper for an output stream.
+ * @param out the underlying input stream
+ * @param bufferSize the buffer size in bytes; should be large enough to mitigate
+ * Thread synchronization and context switching overhead
+ * @param writeTimeout the number of milliseconds to block for a write() or flush() before
+ * throwing an InterruptedIOException; 0 blocks indefinitely
+ * @param closeTimeout the number of milliseconds to block for a close() before throwing
+ * an InterruptedIOException; 0 blocks indefinitely, -1 closes the stream in the background
+ */
+ public TimeoutOutputStream(OutputStream out, int bufferSize, long writeTimeout, long closeTimeout) {
+ super(new BufferedOutputStream(out, bufferSize));
+ this.writeTimeout = writeTimeout;
+ this.closeTimeout = closeTimeout;
+ this.iobuffer = new byte[bufferSize];
+ thread = new Thread((Runnable) () -> runThread(), "TimeoutOutputStream");//$NON-NLS-1$
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ /**
+ * Wraps the underlying stream's method.
+ * It may be important to wait for a stream to actually be closed because it
+ * holds an implicit lock on a system resoure (such as a file) while it is
+ * open. Closing a stream may take time if the underlying stream is still
+ * servicing a previous request.
+ * @throws InterruptedIOException if the timeout expired, bytesTransferred will
+ * reflect the number of bytes flushed from the buffer
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public void close() throws IOException {
+ Thread oldThread;
+ synchronized (this) {
+ if (thread == null) return;
+ oldThread = thread;
+ closeRequested = true;
+ thread.interrupt();
+ checkError();
+ }
+ if (closeTimeout == -1) return;
+ try {
+ oldThread.join(closeTimeout);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // we weren't expecting to be interrupted
+ }
+ synchronized (this) {
+ checkError();
+ if (thread != null) throw new InterruptedIOException();
+ }
+ }
+
+ /**
+ * Writes a byte to the stream.
+ * @throws InterruptedIOException if the timeout expired and no data was sent,
+ * bytesTransferred will be zero
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public synchronized void write(int b) throws IOException {
+ syncCommit(true);
+ iobuffer[(head + length) % iobuffer.length] = (byte) b;
+ length++;
+ notify();
+ }
+
+ /**
+ * Writes multiple bytes to the stream.
+ * @throws InterruptedIOException if the timeout expired, bytesTransferred will
+ * reflect the number of bytes sent
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public synchronized void write(byte[] buffer, int off, int len) throws IOException {
+ int amount = 0;
+ try {
+ do {
+ syncCommit(true);
+ while (amount < len && length != iobuffer.length) {
+ iobuffer[(head + length) % iobuffer.length] = buffer[off++];
+ length++;
+ amount++;
+ }
+ } while (amount < len);
+ } catch (InterruptedIOException e) {
+ e.bytesTransferred = amount;
+ throw e;
+ }
+ notify();
+ }
+
+ /**
+ * Flushes the stream.
+ * @throws InterruptedIOException if the timeout expired, bytesTransferred will
+ * reflect the number of bytes flushed from the buffer
+ * @throws IOException if an i/o error occurs
+ */
+ @Override
+ public synchronized void flush() throws IOException {
+ int oldLength = length;
+ flushRequested = true;
+ try {
+ syncCommit(false);
+ } catch (InterruptedIOException e) {
+ e.bytesTransferred = oldLength - length;
+ throw e;
+ }
+ notify();
+ }
+
+ /**
+ * Waits for the buffer to drain if it is full.
+ * @param partial if true, waits until the buffer is partially empty, else drains it entirely
+ * @throws InterruptedIOException if the buffer could not be drained as requested
+ */
+ private void syncCommit(boolean partial) throws IOException {
+ checkError(); // check errors before allowing the addition of new bytes
+ if (partial && length != iobuffer.length || length == 0) return;
+ if (waitingForClose) throw new IOException(Messages.TimeoutOutputStream_cannotWriteToStream);
+ notify();
+ try {
+ wait(writeTimeout);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // we weren't expecting to be interrupted
+ }
+ checkError(); // check errors before allowing the addition of new bytes
+ if (partial && length != iobuffer.length || length == 0) return;
+ throw new InterruptedIOException();
+ }
+
+ /**
+ * If an exception is pending, throws it.
+ */
+ private void checkError() throws IOException {
+ if (ioe != null) {
+ IOException e = ioe;
+ ioe = null;
+ throw e;
+ }
+ }
+
+ /**
+ * Runs the thread in the background.
+ */
+ private void runThread() {
+ try {
+ writeUntilDone();
+ } catch (IOException e) {
+ synchronized (this) { ioe = e; }
+ } finally {
+ waitUntilClosed();
+ try {
+ out.close();
+ } catch (IOException e) {
+ synchronized (this) { ioe = e; }
+ } finally {
+ synchronized (this) {
+ thread = null;
+ notify();
+ }
+ }
+ }
+ }
+
+ /**
+ * Waits until we have been requested to close the stream.
+ */
+ private synchronized void waitUntilClosed() {
+ waitingForClose = true;
+ notify();
+ while (! closeRequested) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ closeRequested = true; // alternate quit signal
+ }
+ }
+ }
+
+ /**
+ * Writes bytes from the buffer until closed and buffer is empty
+ */
+ private void writeUntilDone() throws IOException {
+ int bytesUntilFlush = -1; // if > 0, then we will flush after that many bytes have been written
+ for (;;) {
+ int off, len;
+ synchronized (this) {
+ for (;;) {
+ if (closeRequested && length == 0) return; // quit signal
+ if (length != 0 || flushRequested) break;
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ closeRequested = true; // alternate quit signal
+ }
+ }
+ off = head;
+ len = iobuffer.length - head;
+ if (len > length) len = length;
+ if (flushRequested && bytesUntilFlush < 0) {
+ flushRequested = false;
+ bytesUntilFlush = length;
+ }
+ }
+
+ // If there are bytes to be written, write them
+ if (len != 0) {
+ // write out all remaining bytes from the buffer before flushing
+ try {
+ // the i/o operation might block without releasing the lock,
+ // so we do this outside of the synchronized block
+ out.write(iobuffer, off, len);
+ } catch (InterruptedIOException e) {
+ len = e.bytesTransferred;
+ }
+ }
+
+ // If there was a pending flush, do it
+ if (bytesUntilFlush >= 0) {
+ bytesUntilFlush -= len;
+ if (bytesUntilFlush <= 0) {
+ // flush the buffer now
+ try {
+ out.flush();
+ } catch (InterruptedIOException e) {
+ }
+ bytesUntilFlush = -1; // might have been 0
+ }
+ }
+
+ // If bytes were written, update the circular buffer
+ if (len != 0) {
+ synchronized (this) {
+ head = (head + len) % iobuffer.length;
+ length -= len;
+ notify();
+ }
+ }
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/AbstractContentComparator.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/AbstractContentComparator.java
new file mode 100644
index 000000000..9a0cf1fe2
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/AbstractContentComparator.java
@@ -0,0 +1,106 @@
+/*******************************************************************************
+ * Copyright (c) 2011 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.internal.core.subscribers;
+
+import java.io.*;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.history.IFileRevision;
+import org.eclipse.team.core.variants.IResourceVariant;
+import org.eclipse.team.internal.core.Policy;
+import org.eclipse.team.internal.core.TeamPlugin;
+
+/**
+ * Compare local and remote contents.
+ *
+ * This comparator makes use of the <code>IStorage</code> provided by
+ * an <code>IResourceVariant</code> or an <code>IFileRevision</code>to obtain the remote contents.
+ * This means that the comparison may contact the server unless the contents
+ * were cached locally by a previous operation. The caching of remote
+ * contents is subscriber specific.
+ */
+public abstract class AbstractContentComparator {
+ private boolean ignoreWhitespace = false;
+
+ public AbstractContentComparator(boolean ignoreWhitespace) {
+ this.ignoreWhitespace = ignoreWhitespace;
+ }
+
+ public boolean compare(IResource e1, IResourceVariant e2, IProgressMonitor monitor) {
+ return compareObjects(e1, e2, monitor);
+ }
+
+ public boolean compare(IResource e1, IFileRevision e2, IProgressMonitor monitor) {
+ return compareObjects(e1, e2, monitor);
+ }
+
+ private boolean compareObjects(Object e1, Object e2, IProgressMonitor monitor) {
+ InputStream is1 = null;
+ InputStream is2 = null;
+ try {
+ monitor.beginTask(null, 100);
+ is1 = getContents(e1, Policy.subMonitorFor(monitor, 30));
+ is2 = getContents(e2, Policy.subMonitorFor(monitor, 30));
+ return contentsEqual(Policy.subMonitorFor(monitor, 40), is1, is2, shouldIgnoreWhitespace());
+ } catch (TeamException e) {
+ TeamPlugin.log(e);
+ return false;
+ } finally {
+ try {
+ try {
+ if (is1 != null) {
+ is1.close();
+ }
+ } finally {
+ if (is2 != null) {
+ is2.close();
+ }
+ }
+ } catch (IOException e) {
+ // Ignore
+ }
+ monitor.done();
+ }
+ }
+
+ protected boolean shouldIgnoreWhitespace() {
+ return ignoreWhitespace;
+ }
+
+ abstract protected boolean contentsEqual(IProgressMonitor monitor, InputStream is1, InputStream is2,
+ boolean ignoreWhitespace);
+
+ private InputStream getContents(Object resource, IProgressMonitor monitor)
+ throws TeamException {
+ try {
+ if (resource instanceof IFile) {
+ return new BufferedInputStream(((IFile) resource).getContents());
+ } else if (resource instanceof IResourceVariant) {
+ IResourceVariant remote = (IResourceVariant) resource;
+ if (!remote.isContainer()) {
+ return new BufferedInputStream(remote.getStorage(monitor)
+ .getContents());
+ }
+ } else if (resource instanceof IFileRevision) {
+ IFileRevision remote = (IFileRevision) resource;
+ return new BufferedInputStream(remote.getStorage(monitor)
+ .getContents());
+ }
+ return null;
+ } catch (CoreException e) {
+ throw TeamException.asTeamException(e);
+ }
+ }
+} \ No newline at end of file
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/AbstractSynchronizationScope.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/AbstractSynchronizationScope.java
new file mode 100644
index 000000000..1c2c59439
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/AbstractSynchronizationScope.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.mapping.ResourceMapping;
+import org.eclipse.core.resources.mapping.ResourceTraversal;
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.mapping.ISynchronizationScope;
+import org.eclipse.team.core.mapping.ISynchronizationScopeChangeListener;
+
+/**
+ * An abstract implementation of an {@link ISynchronizationScope}.
+ *
+ * @since 3.2
+ */
+public abstract class AbstractSynchronizationScope implements ISynchronizationScope {
+
+ private ListenerList<ISynchronizationScopeChangeListener> listeners = new ListenerList<>(ListenerList.IDENTITY);
+
+ @Override
+ public IResource[] getRoots() {
+ List<IResource> result = new ArrayList<>();
+ ResourceTraversal[] traversals = getTraversals();
+ for (int i = 0; i < traversals.length; i++) {
+ ResourceTraversal traversal = traversals[i];
+ IResource[] resources = traversal.getResources();
+ for (int j = 0; j < resources.length; j++) {
+ IResource resource = resources[j];
+ accumulateRoots(result, resource);
+ }
+ }
+ return result.toArray(new IResource[result.size()]);
+ }
+
+ @Override
+ public boolean contains(IResource resource) {
+ ResourceTraversal[] traversals = getTraversals();
+ for (int i = 0; i < traversals.length; i++) {
+ ResourceTraversal traversal = traversals[i];
+ if (traversal.contains(resource))
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Add the resource to the list if it isn't there already
+ * or is not a child of an existing resource.
+ */
+ private void accumulateRoots(List<IResource> roots, IResource resource) {
+ IPath resourcePath = resource.getFullPath();
+ for (Iterator iter = roots.iterator(); iter.hasNext();) {
+ IResource root = (IResource) iter.next();
+ IPath rootPath = root.getFullPath();
+ // If there is a higher resource in the collection, skip this one
+ if (rootPath.isPrefixOf(resourcePath))
+ return;
+ // If there are lower resources, remove them
+ if (resourcePath.isPrefixOf(rootPath))
+ iter.remove();
+ }
+ // There were no higher resources, so add this one
+ roots.add(resource);
+ }
+
+ /**
+ * Fire the scope change event
+ * @param newTraversals the new traversals (may be empty)
+ * @param newMappings the new mappings (may be empty)
+ */
+ public void fireTraversalsChangedEvent(final ResourceTraversal[] newTraversals, final ResourceMapping[] newMappings) {
+ Object[] allListeners = listeners.getListeners();
+ for (int i = 0; i < allListeners.length; i++) {
+ final Object listener = allListeners[i];
+ SafeRunner.run(new ISafeRunnable() {
+ @Override
+ public void run() throws Exception {
+ ((ISynchronizationScopeChangeListener)listener).scopeChanged(AbstractSynchronizationScope.this, newMappings, newTraversals);
+ }
+ @Override
+ public void handleException(Throwable exception) {
+ // Logged by Platform
+ }
+ });
+ }
+ }
+
+ @Override
+ public void addScopeChangeListener(ISynchronizationScopeChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeScopeChangeListener(ISynchronizationScopeChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ActiveChangeSet.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ActiveChangeSet.java
new file mode 100644
index 000000000..beb0b7dee
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ActiveChangeSet.java
@@ -0,0 +1,228 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.diff.IDiff;
+import org.eclipse.team.core.mapping.provider.ResourceDiffTree;
+import org.eclipse.team.internal.core.TeamPlugin;
+import org.osgi.service.prefs.Preferences;
+
+/**
+ * An active change set represents a set of local resource changes
+ * that are grouped together as a single logical change.
+ * @since 3.1
+ */
+public class ActiveChangeSet extends DiffChangeSet {
+
+ private static final String CTX_TITLE = "title"; //$NON-NLS-1$
+ private static final String CTX_COMMENT = "comment"; //$NON-NLS-1$
+ private static final String CTX_RESOURCES = "resources"; //$NON-NLS-1$
+ private static final String CTX_USER_CREATED = "userCreated"; //$NON-NLS-1$
+
+ private final ActiveChangeSetManager manager;
+ private String comment;
+ private boolean userCreated = true;
+
+ /**
+ * Create a change set with the given title
+ * @param manager the manager that owns this set
+ * @param title the title of the set
+ */
+ public ActiveChangeSet(ActiveChangeSetManager manager, String title) {
+ super(title);
+ this.manager = manager;
+ }
+
+ /**
+ * Get the title of the change set. The title is used
+ * as the comment when the set is checking in if no comment
+ * has been explicitly set using <code>setComment</code>.
+ * @return the title of the set
+ */
+ public String getTitle() {
+ return getName();
+ }
+
+ /**
+ * Set the title of the set. The title is used
+ * as the comment when the set is committed if no comment
+ * has been explicitly set using <code>setComment</code>.
+ * @param title the title of the set
+ */
+ public void setTitle(String title) {
+ setName(title);
+ getManager().fireNameChangedEvent(this);
+ }
+
+ /**
+ * Get the comment of this change set. If the comment
+ * as never been set, the title is returned as the comment
+ * @return the comment to be used when the set is committed
+ */
+ @Override
+ public String getComment() {
+ if (comment == null) {
+ return getTitle();
+ }
+ return comment;
+ }
+
+ /**
+ * Set the comment to be used when the change set is committed.
+ * If <code>null</code> is passed, the title of the set
+ * will be used as the comment.
+ * @param comment the comment for the set or <code>null</code>
+ * if the title should be the comment
+ */
+ public void setComment(String comment) {
+ if (comment != null && comment.equals(getTitle())) {
+ this.comment = null;
+ } else {
+ this.comment = comment;
+ }
+ }
+
+ /*
+ * Override inherited method to only include outgoing changes
+ */
+ @Override
+ protected boolean isValidChange(IDiff diff) {
+ return getManager().isModified(diff);
+ }
+
+ private void addResource(IResource resource) throws CoreException {
+ IDiff diff = getManager().getDiff(resource);
+ if (diff != null) {
+ add(diff);
+ }
+ }
+
+ private ActiveChangeSetManager getManager() {
+ return manager;
+ }
+
+ /**
+ * Return whether the set has a comment that differs from the title.
+ * @return whether the set has a comment that differs from the title
+ */
+ public boolean hasComment() {
+ return comment != null;
+ }
+
+ public void save(Preferences prefs) {
+ prefs.put(CTX_TITLE, getTitle());
+ if (comment != null) {
+ prefs.put(CTX_COMMENT, comment);
+ }
+ if (!isEmpty()) {
+ StringBuilder buffer = new StringBuilder();
+ IResource[] resources = getResources();
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ buffer.append(resource.getFullPath().toString());
+ buffer.append('\n');
+ }
+ prefs.put(CTX_RESOURCES, buffer.toString());
+ }
+ prefs.putBoolean(CTX_USER_CREATED, isUserCreated());
+ }
+
+ public void init(Preferences prefs) {
+ setName(prefs.get(CTX_TITLE, "")); //$NON-NLS-1$
+ comment = prefs.get(CTX_COMMENT, null);
+ String resourcePaths = prefs.get(CTX_RESOURCES, null);
+ if (resourcePaths != null) {
+ ResourceDiffTree tree = internalGetDiffTree();
+ try {
+ tree.beginInput();
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ StringTokenizer tokenizer = new StringTokenizer(resourcePaths, "\n"); //$NON-NLS-1$
+ while (tokenizer.hasMoreTokens()) {
+ String next = tokenizer.nextToken();
+ if (next.trim().length() > 0) {
+ IResource resource = getResource(root, next);
+ // Only include the resource if it is out-of-sync
+ try {
+ if (resource != null && getManager().getDiff(resource) != null) {
+ addResource(resource);
+ }
+ } catch (CoreException e) {
+ TeamPlugin.log(e);
+ }
+ }
+ }
+ } finally {
+ tree.endInput(null);
+ }
+ }
+ userCreated = prefs.getBoolean(CTX_USER_CREATED, true);
+ }
+
+ private IResource getResource(IWorkspaceRoot root, String next) {
+ IResource resource = root.findMember(next);
+ if (resource == null) {
+ // May be an outgoing deletion
+ Path path = new Path(null, next);
+ if (next.charAt(next.length()-1) == IPath.SEPARATOR) {
+ if (path.segmentCount() == 1) {
+ // resource is a project
+ resource = root.getProject(path.lastSegment());
+ } else {
+ // resource is a folder
+ resource = root.getFolder(path);
+ }
+ } else {
+ // resource is a file
+ resource = root.getFile(path);
+ }
+ }
+ return resource;
+ }
+
+ /**
+ * Add the resources to the change set if they are outgoing changes.
+ * @param resources the resources to add.
+ * @throws CoreException
+ */
+ public void add(IResource[] resources) throws CoreException {
+ List<IDiff> toAdd = new ArrayList<>();
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ IDiff diff = getManager().getDiff(resource);
+ if (diff != null) {
+ toAdd.add(diff);
+ }
+ }
+ if (!toAdd.isEmpty()) {
+ add(toAdd.toArray(new IDiff[toAdd.size()]));
+ }
+ }
+
+ /**
+ * Set whether this set was created by the user.
+ * @param userCreated whether this set was created by the user
+ */
+ public void setUserCreated(boolean userCreated) {
+ this.userCreated = userCreated;
+ }
+
+ /**
+ * Return whether this set was created by the user.
+ * @return whether this set was created by the user
+ */
+ public boolean isUserCreated() {
+ return userCreated;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ActiveChangeSetManager.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ActiveChangeSetManager.java
new file mode 100644
index 000000000..b288bef4e
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ActiveChangeSetManager.java
@@ -0,0 +1,444 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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
+ * Matt McCutchen <hashproduct+eclipse@gmail.com> - Bug 128429 [Change Sets] Change Sets with / in name do not get persited
+ *******************************************************************************/
+package org.eclipse.team.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.resources.mapping.ResourceTraversal;
+import org.eclipse.core.runtime.*;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.diff.*;
+import org.eclipse.team.core.mapping.IChangeGroupingRequestor;
+import org.eclipse.team.core.mapping.IResourceDiffTree;
+import org.eclipse.team.internal.core.Messages;
+import org.eclipse.team.internal.core.TeamPlugin;
+import org.eclipse.team.internal.core.mapping.CompoundResourceTraversal;
+import org.osgi.service.prefs.BackingStoreException;
+import org.osgi.service.prefs.Preferences;
+
+/**
+ * A change set manager that contains sets that represent collections of
+ * related local changes.
+ */
+public abstract class ActiveChangeSetManager extends ChangeSetManager implements IDiffChangeListener, IChangeGroupingRequestor {
+
+ private static final String CTX_DEFAULT_SET = "defaultSet"; //$NON-NLS-1$
+
+ private ActiveChangeSet defaultSet;
+
+ /**
+ * Return the Change Set whose sync info set is the
+ * one given.
+ * @param tree a diff tree
+ * @return the change set for the given diff tree
+ */
+ protected ChangeSet getChangeSet(IResourceDiffTree tree) {
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ChangeSet changeSet = sets[i];
+ if (((DiffChangeSet)changeSet).getDiffTree() == tree) {
+ return changeSet;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void add(ChangeSet set) {
+ Assert.isTrue(set instanceof ActiveChangeSet);
+ super.add(set);
+ }
+
+ @Override
+ protected void handleSetAdded(ChangeSet set) {
+ Assert.isTrue(set instanceof ActiveChangeSet);
+ ((DiffChangeSet)set).getDiffTree().addDiffChangeListener(getDiffTreeListener());
+ super.handleSetAdded(set);
+ handleAddedResources(set, ((ActiveChangeSet)set).internalGetDiffTree().getDiffs());
+ }
+
+ @Override
+ protected void handleSetRemoved(ChangeSet set) {
+ ((DiffChangeSet)set).getDiffTree().removeDiffChangeListener(getDiffTreeListener());
+ super.handleSetRemoved(set);
+ }
+
+ /**
+ * Return the listener that is registered with the diff trees associated with
+ * the sets for this manager.
+ * @return the listener that is registered with the diff trees associated with
+ * the sets for this manager
+ */
+ protected IDiffChangeListener getDiffTreeListener() {
+ return this;
+ }
+
+ @Override
+ public void diffsChanged(IDiffChangeEvent event, IProgressMonitor monitor) {
+ IResourceDiffTree tree = (IResourceDiffTree)event.getTree();
+ handleSyncSetChange(tree, event.getAdditions(), getAllResources(event));
+ }
+
+ @Override
+ public void propertyChanged(IDiffTree tree, int property, IPath[] paths) {
+ // ignore
+ }
+
+ @Override
+ public boolean isModified(IFile file) throws CoreException {
+ IDiff diff = getDiff(file);
+ if (diff != null)
+ return isModified(diff);
+ return false;
+ }
+
+ /**
+ * Return whether the given diff represents a local change.
+ * @param diff the diff
+ * @return whether the given diff represents a local change
+ */
+ public boolean isModified(IDiff diff) {
+ if (diff != null) {
+ if (diff instanceof IThreeWayDiff) {
+ IThreeWayDiff twd = (IThreeWayDiff) diff;
+ int dir = twd.getDirection();
+ return dir == IThreeWayDiff.OUTGOING || dir == IThreeWayDiff.CONFLICTING;
+ } else {
+ return diff.getKind() != IDiff.NO_CHANGE;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the set with the given name.
+ * @param name the name of the set
+ * @return the set with the given name
+ */
+ public ActiveChangeSet getSet(String name) {
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ChangeSet set = sets[i];
+ if (set.getName().equals(name) && set instanceof ActiveChangeSet) {
+ return (ActiveChangeSet)set;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Create a change set containing the given files if
+ * they have been modified locally.
+ * @param title the title of the commit set
+ * @param files the files contained in the set
+ * @return the created set
+ * @throws CoreException
+ */
+ public ActiveChangeSet createSet(String title, IFile[] files) throws CoreException {
+ List<IDiff> infos = new ArrayList<>();
+ for (int i = 0; i < files.length; i++) {
+ IFile file = files[i];
+ IDiff diff = getDiff(file);
+ if (diff != null) {
+ infos.add(diff);
+ }
+ }
+ return createSet(title, infos.toArray(new IDiff[infos.size()]));
+ }
+
+ /**
+ * Create a commit set with the given title and files. The created
+ * set is not added to the control of the commit set manager
+ * so no events are fired. The set can be added using the
+ * <code>add</code> method.
+ * @param title the title of the commit set
+ * @param diffs the files contained in the set
+ * @return the created set
+ */
+ public ActiveChangeSet createSet(String title, IDiff[] diffs) {
+ ActiveChangeSet commitSet = doCreateSet(title);
+ if (diffs != null && diffs.length > 0) {
+ commitSet.add(diffs);
+ }
+ return commitSet;
+ }
+
+ /**
+ * Create a change set with the given name.
+ * @param name the name of the change set
+ * @return the created change set
+ */
+ protected ActiveChangeSet doCreateSet(String name) {
+ return new ActiveChangeSet(this, name);
+ }
+
+ public abstract IDiff getDiff(IResource resource) throws CoreException;
+
+ /**
+ * Return whether the manager allows a resource to
+ * be in multiple sets. By default, a resource
+ * may only be in one set.
+ * @return whether the manager allows a resource to
+ * be in multiple sets.
+ */
+ protected boolean isSingleSetPerResource() {
+ return true;
+ }
+
+ private IPath[] getAllResources(IDiffChangeEvent event) {
+ Set<IPath> allResources = new HashSet<>();
+ IDiff[] addedResources = event.getAdditions();
+ for (int i = 0; i < addedResources.length; i++) {
+ IDiff diff = addedResources[i];
+ allResources.add(diff.getPath());
+ }
+ IDiff[] changedResources = event.getChanges();
+ for (int i = 0; i < changedResources.length; i++) {
+ IDiff diff = changedResources[i];
+ allResources.add(diff.getPath());
+ }
+ IPath[] removals = event.getRemovals();
+ for (int i = 0; i < removals.length; i++) {
+ IPath path = removals[i];
+ allResources.add(path);
+ }
+ return allResources.toArray(new IPath[allResources.size()]);
+ }
+
+ /**
+ * React to the given diffs being added to the given set.
+ * @param set the set
+ * @param diffs the diffs
+ */
+ protected void handleAddedResources(ChangeSet set, IDiff[] diffs) {
+ if (isSingleSetPerResource() && ((ActiveChangeSet)set).isUserCreated()) {
+ IResource[] resources = new IResource[diffs.length];
+ for (int i = 0; i < resources.length; i++) {
+ resources[i] = ((DiffChangeSet)set).getDiffTree().getResource(diffs[i]);
+ }
+ // Remove the added files from any other set that contains them
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ChangeSet otherSet = sets[i];
+ if (otherSet != set && ((ActiveChangeSet)otherSet).isUserCreated()) {
+ otherSet.remove(resources);
+ }
+ }
+ }
+ }
+
+ private void handleSyncSetChange(IResourceDiffTree tree, IDiff[] addedDiffs, IPath[] allAffectedResources) {
+ ChangeSet changeSet = getChangeSet(tree);
+ if (tree.isEmpty() && changeSet != null) {
+ remove(changeSet);
+ }
+ fireResourcesChangedEvent(changeSet, allAffectedResources);
+ handleAddedResources(changeSet, addedDiffs);
+ }
+
+ /**
+ * Make the given set the default set into which all new modifications that
+ * are not already in another set go.
+ *
+ * @param set
+ * the set which is to become the default set or
+ * <code>null</code> to unset the default set
+ */
+ public void makeDefault(ActiveChangeSet set) {
+ // The default set must be an active set
+ if (set != null && !contains(set)) {
+ add(set);
+ }
+ ActiveChangeSet oldSet = defaultSet;
+ defaultSet = set;
+ fireDefaultChangedEvent(oldSet, defaultSet);
+ }
+
+ /**
+ * Return whether the given set is the default set into which all
+ * new modifications will be placed.
+ * @param set the set to test
+ * @return whether the set is the default set
+ */
+ public boolean isDefault(ActiveChangeSet set) {
+ return set == defaultSet;
+ }
+
+ /**
+ * Return the set which is currently the default or
+ * <code>null</code> if there is no default set.
+ * @return the default change set
+ */
+ public ActiveChangeSet getDefaultSet() {
+ return defaultSet;
+ }
+
+ /**
+ * If the given traversals contain any resources in the active change sets, ensure
+ * that the traversals cover all the resources in the overlapping change set.
+ * @param traversals the traversals
+ * @return the traversals adjusted to contain all the resources of intersecting change sets
+ */
+ public ResourceTraversal[] adjustInputTraversals(ResourceTraversal[] traversals) {
+ CompoundResourceTraversal traversal = new CompoundResourceTraversal();
+ traversal.addTraversals(traversals);
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ChangeSet set = sets[i];
+ handleIntersect(traversal, set);
+ }
+ return traversal.asTraversals();
+ }
+
+ private void handleIntersect(CompoundResourceTraversal traversal, ChangeSet set) {
+ IResource[] resources = set.getResources();
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ if (traversal.isCovered(resource, IResource.DEPTH_ZERO)) {
+ traversal.addResources(resources, IResource.DEPTH_ZERO);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Save the state of this manager including all its contained sets
+ * into the given preferences node.
+ * @param prefs a preferences node
+ */
+ protected void save(Preferences prefs) {
+ // No need to save the sets if the manager has never been initialized
+ if (!isInitialized())
+ return;
+ // Clear the persisted state before saving the new state
+ try {
+ String[] oldSetNames = prefs.childrenNames();
+ for (int i = 0; i < oldSetNames.length; i++) {
+ String string = oldSetNames[i];
+ prefs.node(string).removeNode();
+ }
+ } catch (BackingStoreException e) {
+ TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_5, new String[] { getName() }), e);
+ }
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ChangeSet set = sets[i];
+ if (set instanceof ActiveChangeSet && !set.isEmpty()) {
+ // Since the change set title is stored explicitly, the name of
+ // the child preference node doesn't matter as long as it
+ // doesn't contain / and no two change sets get the same name.
+ String childPrefName = escapePrefName(((ActiveChangeSet)set).getTitle());
+ Preferences child = prefs.node(childPrefName);
+ ((ActiveChangeSet)set).save(child);
+ }
+ }
+ if (getDefaultSet() != null) {
+ prefs.put(CTX_DEFAULT_SET, getDefaultSet().getTitle());
+ } else {
+ // unset default changeset
+ prefs.remove(CTX_DEFAULT_SET);
+ }
+ try {
+ prefs.flush();
+ } catch (BackingStoreException e) {
+ TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_3, new String[] { getName() }), e);
+ }
+ }
+
+ /**
+ * Escape the given string for safe use as a preference node name by
+ * translating / to \s (so it's a single path component) and \ to \\ (to
+ * preserve uniqueness).
+ *
+ * @param string
+ * Input string
+ * @return Escaped output string
+ */
+ private static String escapePrefName(String string) {
+ StringBuilder out = new StringBuilder();
+ for (int i = 0; i < string.length(); i++) {
+ char c = string.charAt(i);
+ switch (c) {
+ case '/':
+ out.append("\\s"); //$NON-NLS-1$
+ break;
+ case '\\':
+ out.append("\\\\"); //$NON-NLS-1$
+ break;
+ default:
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+
+ /**
+ * Load the manager's state from the given preferences node.
+ *
+ * @param prefs
+ * a preferences node
+ */
+ protected void load(Preferences prefs) {
+ String defaultSetTitle = prefs.get(CTX_DEFAULT_SET, null);
+ try {
+ String[] childNames = prefs.childrenNames();
+ for (int i = 0; i < childNames.length; i++) {
+ String string = childNames[i];
+ Preferences childPrefs = prefs.node(string);
+ ActiveChangeSet set = createSet(childPrefs);
+ if (!set.isEmpty()) {
+ if (getDefaultSet() == null && defaultSetTitle != null && set.getTitle().equals(defaultSetTitle)) {
+ makeDefault(set);
+ }
+ add(set);
+ }
+ }
+ } catch (BackingStoreException e) {
+ TeamPlugin.log(IStatus.ERROR, NLS.bind(Messages.SubscriberChangeSetCollector_4, new String[] { getName() }), e);
+ }
+ }
+
+ /**
+ * Return the name of this change set manager.
+ * @return the name of this change set manager
+ */
+ protected abstract String getName();
+
+ /**
+ * Create a change set from the given preferences that were
+ * previously saved.
+ * @param childPrefs the previously saved preferences
+ * @return the created change set
+ */
+ protected ActiveChangeSet createSet(Preferences childPrefs) {
+ // Don't specify a title when creating the change set; instead, let the
+ // change set read its title from the preferences.
+ ActiveChangeSet changeSet = doCreateSet(null);
+ changeSet.init(childPrefs);
+ return changeSet;
+ }
+
+ @Override
+ public void ensureChangesGrouped(IProject project, IFile[] files,
+ String name) throws CoreException {
+ ActiveChangeSet set = getSet(name);
+ if (set == null) {
+ set = createSet(name, files);
+ set.setUserCreated(false);
+ add(set);
+ } else {
+ set.setUserCreated(false);
+ set.add(files);
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/BatchingChangeSetManager.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/BatchingChangeSetManager.java
new file mode 100644
index 000000000..7a07c6dc6
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/BatchingChangeSetManager.java
@@ -0,0 +1,179 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.ILock;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.team.internal.core.Policy;
+
+/**
+ * A change set manager that batches change event notification.
+ */
+public class BatchingChangeSetManager extends ChangeSetManager {
+
+ private ILock lock = Job.getJobManager().newLock();
+
+ public static class CollectorChangeEvent {
+
+ Set<ChangeSet> added = new HashSet<>();
+ Set<ChangeSet> removed = new HashSet<>();
+ Map<ChangeSet, IPath[]> changed = new HashMap<>();
+ private final BatchingChangeSetManager collector;
+
+ public CollectorChangeEvent(BatchingChangeSetManager collector) {
+ this.collector = collector;
+ }
+
+ private void setAdded(ChangeSet set) {
+ added.add(set);
+ removed.remove(set);
+ }
+
+ private void setRemoved(ChangeSet set) {
+ added.remove(set);
+ removed.add(set);
+ // Do not clear the changes list since clients may want to know what resources
+ // were changed before the set was removed
+ }
+
+ private void changed(ChangeSet changeSet, IPath[] allAffectedResources) {
+ if (added.contains(changeSet))
+ return;
+ IPath[] paths = changed.get(changeSet);
+ if (paths == null) {
+ changed.put(changeSet, allAffectedResources);
+ } else {
+ Set<IPath> allPaths = new HashSet<>();
+ for (int i = 0; i < paths.length; i++) {
+ IPath path = paths[i];
+ allPaths.add(path);
+ }
+ for (int i = 0; i < allAffectedResources.length; i++) {
+ IPath path = allAffectedResources[i];
+ allPaths.add(path);
+ }
+ changed.put(changeSet, allPaths.toArray(new IPath[allPaths.size()]));
+ }
+ }
+
+ public boolean isEmpty() {
+ return changed.isEmpty() && added.isEmpty() && removed.isEmpty();
+ }
+
+ public ChangeSet[] getAddedSets() {
+ return added.toArray(new ChangeSet[added.size()]);
+ }
+
+ public ChangeSet[] getRemovedSets() {
+ return removed.toArray(new ChangeSet[removed.size()]);
+ }
+
+ public ChangeSet[] getChangedSets() {
+ return changed.keySet().toArray(new ChangeSet[changed.size()]);
+ }
+
+ public IPath[] getChangesFor(ChangeSet set) {
+ return changed.get(set);
+ }
+
+ public BatchingChangeSetManager getSource() {
+ return collector;
+ }
+ }
+
+ public static interface IChangeSetCollectorChangeListener {
+ public void changeSetChanges(CollectorChangeEvent event, IProgressMonitor monitor);
+ }
+
+ private CollectorChangeEvent changes = new CollectorChangeEvent(this);
+
+ public void beginInput() {
+ lock.acquire();
+ }
+
+ 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();
+ }
+ }
+
+ private void fireChanges(final IProgressMonitor monitor) {
+ if (changes.isEmpty()) {
+ return;
+ }
+ final CollectorChangeEvent event = changes;
+ changes = new CollectorChangeEvent(this);
+ Object[] listeners = getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ final IChangeSetChangeListener listener = (IChangeSetChangeListener)listeners[i];
+ if (listener instanceof IChangeSetCollectorChangeListener) {
+ final IChangeSetCollectorChangeListener csccl = (IChangeSetCollectorChangeListener) listener;
+ SafeRunner.run(new ISafeRunnable() {
+ @Override
+ public void handleException(Throwable exception) {
+ // Exceptions are logged by the platform
+ }
+ @Override
+ public void run() throws Exception {
+ csccl.changeSetChanges(event, monitor);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void add(ChangeSet set) {
+ try {
+ beginInput();
+ super.add(set);
+ changes.setAdded(set);
+ } finally {
+ endInput(null);
+ }
+ }
+
+ @Override
+ public void remove(ChangeSet set) {
+ try {
+ beginInput();
+ super.remove(set);
+ changes.setRemoved(set);
+ } finally {
+ endInput(null);
+ }
+ }
+
+ @Override
+ protected void fireResourcesChangedEvent(ChangeSet changeSet, IPath[] allAffectedResources) {
+ super.fireResourcesChangedEvent(changeSet, allAffectedResources);
+ try {
+ beginInput();
+ changes.changed(changeSet, allAffectedResources);
+ } finally {
+ endInput(null);
+ }
+ }
+
+ @Override
+ protected void initializeSets() {
+ // Nothing to do
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/BatchingLock.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/BatchingLock.java
new file mode 100644
index 000000000..b4793ab7a
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/BatchingLock.java
@@ -0,0 +1,334 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.*;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.internal.core.*;
+
+/**
+ * Provides a per-thread nested locking mechanism. A thread can acquire a
+ * lock on a specific resource by calling acquire(). Subsequently, acquire() can be called
+ * multiple times on the resource or any of its children from within the same thread
+ * without blocking. Other threads that try
+ * and acquire the lock on those same resources will be blocked until the first
+ * thread releases all it's nested locks.
+ * <p>
+ * The locking is managed by the platform via scheduling rules. This class simply
+ * provides the nesting mechanism in order to allow the client to determine when
+ * the lock for the thread has been released. Therefore, this lock will block if
+ * another thread already locks the same resource.</p>
+ */
+public class BatchingLock {
+ // This is a placeholder rule used to indicate that no scheduling rule is needed
+ /* internal use only */ static final ISchedulingRule NULL_SCHEDULING_RULE= new ISchedulingRule() {
+ @Override
+ public boolean contains(ISchedulingRule rule) {
+ return false;
+ }
+ @Override
+ public boolean isConflicting(ISchedulingRule rule) {
+ return false;
+ }
+ };
+
+ public class ThreadInfo {
+ private Set<IResource> changedResources = new HashSet<>();
+ private IFlushOperation operation;
+ private List<ISchedulingRule> rules = new ArrayList<>();
+ public ThreadInfo(IFlushOperation operation) {
+ this.operation = operation;
+ }
+ /**
+ * Push a scheduling rule onto the stack for this thread and
+ * acquire the rule if it is not the workspace root.
+ * @param resource
+ * @param monitor
+ * @return the scheduling rule that was obtained
+ */
+ public ISchedulingRule pushRule(ISchedulingRule resource, IProgressMonitor monitor) {
+ // The scheduling rule is either the project or the resource's parent
+ final ISchedulingRule rule = getRuleForResoure(resource);
+ if (rule != NULL_SCHEDULING_RULE) {
+ boolean success = false;
+ try {
+ Job.getJobManager().beginRule(rule, monitor);
+ addRule(rule);
+ success = true;
+ } finally {
+ if (!success) {
+ try {
+ // The begin was canceled (or some other problem occurred).
+ // Free the scheduling rule
+ // so the clients of ReentrantLock don't need to
+ // do an endRule when the operation is canceled.
+ Job.getJobManager().endRule(rule);
+ } catch (RuntimeException e) {
+ // Log and ignore so the original exception is not lost
+ TeamPlugin.log(IStatus.ERROR, "Failed to end scheduling rule", e); //$NON-NLS-1$
+ }
+ }
+ }
+ } else {
+ // Record the fact that we didn't push a rule so we
+ // can match it when we pop
+ addRule(rule);
+ }
+ return rule;
+ }
+ /**
+ * Pop the scheduling rule from the stack and release it if it
+ * is not the workspace root. Flush any changed sync info to
+ * disk if necessary. A flush is necessary if the stack is empty
+ * or if the top-most non-null scheduling rule was popped as a result
+ * of this operation.
+ * @param rule
+ * @param monitor
+ * @throws TeamException
+ */
+ public void popRule(ISchedulingRule rule, IProgressMonitor monitor) throws TeamException {
+ try {
+ if (isFlushRequired()) {
+ flush(monitor);
+ }
+ } finally {
+ ISchedulingRule stackedRule = removeRule();
+ if (rule == null) {
+ rule = NULL_SCHEDULING_RULE;
+ }
+ Assert.isTrue(stackedRule.equals(rule), "end for resource '" + rule + "' does not match stacked rule '" + stackedRule + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ if (rule != NULL_SCHEDULING_RULE) {
+ Job.getJobManager().endRule(rule);
+ }
+ }
+ }
+ private ISchedulingRule getRuleForResoure(ISchedulingRule resourceRule) {
+ ISchedulingRule rule;
+ if (resourceRule instanceof IResource) {
+ IResource resource = (IResource)resourceRule;
+ if (resource.getType() == IResource.ROOT) {
+ // Never lock the whole workspace
+ rule = NULL_SCHEDULING_RULE;
+ } else if (resource.getType() == IResource.PROJECT) {
+ rule = resource;
+ } else {
+ rule = resource.getParent();
+ }
+ } else if (resourceRule instanceof MultiRule) {
+ // Create a MultiRule for all projects from the given rule
+ ISchedulingRule[] rules = ((MultiRule)resourceRule).getChildren();
+ Set<ISchedulingRule> projects = new HashSet<>();
+ for (int i = 0; i < rules.length; i++) {
+ ISchedulingRule childRule = rules[i];
+ if (childRule instanceof IResource) {
+ projects.add(((IResource)childRule).getProject());
+ }
+ }
+ if (projects.isEmpty()) {
+ rule = NULL_SCHEDULING_RULE;
+ } else if (projects.size() == 1) {
+ rule = projects.iterator().next();
+ } else {
+ rule = new MultiRule(projects.toArray(new ISchedulingRule[projects.size()]));
+ }
+ } else {
+ // Rule is not associated with resources so ignore it
+ rule = NULL_SCHEDULING_RULE;
+ }
+ return rule;
+ }
+ /**
+ * Return <code>true</code> if we are still nested in
+ * an acquire for this thread.
+ *
+ * @return whether there are still rules on the stack
+ */
+ public boolean isNested() {
+ return !rules.isEmpty();
+ }
+ public void addChangedResource(IResource resource) {
+ changedResources.add(resource);
+ }
+ public boolean isEmpty() {
+ return changedResources.isEmpty();
+ }
+ public IResource[] getChangedResources() {
+ return changedResources.toArray(new IResource[changedResources.size()]);
+ }
+ public void flush(IProgressMonitor monitor) throws TeamException {
+ try {
+ operation.flush(this, monitor);
+ } catch (OutOfMemoryError e) {
+ throw e;
+ } catch (Error e) {
+ handleAbortedFlush(e);
+ throw e;
+ } catch (RuntimeException e) {
+ handleAbortedFlush(e);
+ throw e;
+ } finally {
+ // We have to clear the resources no matter what since the next attempt
+ // to flush may not have an appropriate scheduling rule
+ changedResources.clear();
+ }
+ }
+ private boolean isFlushRequired() {
+ return rules.size() == 1 || remainingRulesAreNull();
+ }
+ /*
+ * Return true if all but the last rule in the stack is null
+ */
+ private boolean remainingRulesAreNull() {
+ for (int i = 0; i < rules.size() - 1; i++) {
+ ISchedulingRule rule = rules.get(i);
+ if (rule != NULL_SCHEDULING_RULE) {
+ return false;
+ }
+ }
+ return true;
+ }
+ private void handleAbortedFlush(Throwable t) {
+ TeamPlugin.log(IStatus.ERROR, Messages.BatchingLock_11, t);
+ }
+ private void addRule(ISchedulingRule rule) {
+ rules.add(rule);
+ }
+ private ISchedulingRule removeRule() {
+ return rules.remove(rules.size() - 1);
+ }
+ public boolean ruleContains(IResource resource) {
+ for (Iterator iter = rules.iterator(); iter.hasNext();) {
+ ISchedulingRule rule = (ISchedulingRule) iter.next();
+ if (rule != NULL_SCHEDULING_RULE && rule.contains(resource)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public interface IFlushOperation {
+ public void flush(ThreadInfo info, IProgressMonitor monitor) throws TeamException;
+ }
+
+ private Map<Thread, ThreadInfo> infos = new HashMap<>();
+
+ /**
+ * Return the thread info for the current thread
+ * @return the thread info for the current thread
+ */
+ protected ThreadInfo getThreadInfo() {
+ Thread thisThread = Thread.currentThread();
+ synchronized (infos) {
+ ThreadInfo info = infos.get(thisThread);
+ return info;
+ }
+ }
+
+ private ThreadInfo getThreadInfo(IResource resource) {
+ synchronized (infos) {
+ for (Iterator iter = infos.values().iterator(); iter.hasNext();) {
+ ThreadInfo info = (ThreadInfo) iter.next();
+ if (info.ruleContains(resource)) {
+ return info;
+ }
+ }
+ return null;
+ }
+ }
+
+ public ISchedulingRule acquire(ISchedulingRule resourceRule, IFlushOperation operation, IProgressMonitor monitor) {
+ ThreadInfo info = getThreadInfo();
+ boolean added = false;
+ synchronized (infos) {
+ if (info == null) {
+ info = createThreadInfo(operation);
+ Thread thisThread = Thread.currentThread();
+ infos.put(thisThread, info);
+ added = true;
+ if(Policy.DEBUG_THREADING) System.out.println("[" + thisThread.getName() + "] acquired batching lock on " + resourceRule); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ }
+ try {
+ return info.pushRule(resourceRule, monitor);
+ } catch (OperationCanceledException e) {
+ // The operation was canceled.
+ // If this is the outermost acquire then remove the info that was just added
+ if (added) {
+ synchronized (infos) {
+ infos.remove(Thread.currentThread());
+ }
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Create the ThreadInfo instance used to cache the lock state for the
+ * current thread. Subclass can override to provide a subclass of
+ * ThreadInfo.
+ * @param operation the flush operation
+ * @return a ThreadInfo instance
+ */
+ protected ThreadInfo createThreadInfo(IFlushOperation operation) {
+ return new ThreadInfo(operation);
+ }
+
+ /**
+ * Release the lock held on any resources by this thread. The provided rule must
+ * be identical to the rule returned by the corresponding acquire(). If the rule
+ * for the release is non-null and all remaining rules held by the lock are null,
+ * the the flush operation provided in the acquire method will be executed.
+ * @param rule the scheduling rule
+ * @param monitor a progress monitor
+ * @throws TeamException
+ */
+ public void release(ISchedulingRule rule, IProgressMonitor monitor) throws TeamException {
+ ThreadInfo info = getThreadInfo();
+ Assert.isNotNull(info, "Unmatched acquire/release."); //$NON-NLS-1$
+ Assert.isTrue(info.isNested(), "Unmatched acquire/release."); //$NON-NLS-1$
+ info.popRule(rule, monitor);
+ synchronized (infos) {
+ if (!info.isNested()) {
+ Thread thisThread = Thread.currentThread();
+ if(Policy.DEBUG_THREADING) System.out.println("[" + thisThread.getName() + "] released batching lock"); //$NON-NLS-1$ //$NON-NLS-2$
+ infos.remove(thisThread);
+ }
+ }
+ }
+
+ public void resourceChanged(IResource resource) {
+ ThreadInfo info = getThreadInfo();
+ Assert.isNotNull(info, "Folder changed outside of resource lock"); //$NON-NLS-1$
+ info.addChangedResource(resource);
+ }
+
+ /**
+ * Flush any changes accumulated by the lock so far.
+ * @param monitor a progress monitor
+ * @throws TeamException
+ */
+ public void flush(IProgressMonitor monitor) throws TeamException {
+ ThreadInfo info = getThreadInfo();
+ Assert.isNotNull(info, "Flush requested outside of resource lock"); //$NON-NLS-1$
+ info.flush(monitor);
+ }
+
+ public boolean isWithinActiveOperationScope(IResource resource) {
+ synchronized (infos) {
+ return getThreadInfo(resource) != null;
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ChangeSet.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ChangeSet.java
new file mode 100644
index 000000000..31d374fbc
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ChangeSet.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * Copyright (c) 2006 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IResource;
+
+/**
+ * A set that contains a group of related changes
+ */
+public abstract class ChangeSet {
+
+ private String name;
+
+ /**
+ * Create a change set with no name. Subclasses
+ * that create a change set in this manner should
+ * provide a name before the set is used by other clients.
+ */
+ protected ChangeSet() {
+ super();
+ }
+
+ /**
+ * Create a change set with the given name.
+ * @param name the name of the change set
+ */
+ public ChangeSet(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Return the resources that are contained in this set.
+ * @return the resources that are contained in this set
+ */
+ public abstract IResource[] getResources();
+
+ /**
+ * Return whether the set contains any files.
+ * @return whether the set contains any files
+ */
+ public abstract boolean isEmpty();
+
+ /**
+ * Return true if the given file is included in this set.
+ * @param local a local file
+ * @return true if the given file is included in this set
+ */
+ public abstract boolean contains(IResource local);
+
+ /**
+ * Remove the resource from the set.
+ * @param resource the resource to be removed
+ */
+ public abstract void remove(IResource resource);
+
+ /**
+ * Remove the resources from the set.
+ * @param resources the resources to be removed
+ */
+ public void remove(IResource[] resources) {
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ remove(resource);
+ }
+ }
+
+ /**
+ * Remove the resource and it's descendants to the given depth.
+ * @param resource the resource to be removed
+ * @param depth the depth of the removal (one of
+ * <code>IResource.DEPTH_ZERO, IResource.DEPTH_ONE, IResource.DEPTH_INFINITE)</code>
+ */
+ public abstract void rootRemoved(IResource resource, int depth);
+
+ /**
+ * Return a comment describing the change.
+ * @return a comment describing the change
+ */
+ public abstract String getComment();
+
+ /**
+ * Return the name assigned to this set. The name should be
+ * unique.
+ * @return the name assigned to this set
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Set the name of the change set. The name of a change
+ * set can be changed but it is up to subclass to notify
+ * any interested partied of the name change.
+ * @param name the new name for the set
+ */
+ protected void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Return whether the set contains descendants of the given resource
+ * to the given depth.
+ * @param resource the resource
+ * @param depth the depth
+ * @return whether the set contains descendants of the given resource
+ * to the given depth
+ */
+ public abstract boolean containsChildren(IResource resource, int depth);
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ChangeSetManager.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ChangeSetManager.java
new file mode 100644
index 000000000..f72362c6a
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ChangeSetManager.java
@@ -0,0 +1,240 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.runtime.*;
+
+/**
+ * An abstract class that managers a collection of change sets.
+ */
+public abstract class ChangeSetManager {
+
+ private ListenerList<IChangeSetChangeListener> listeners = new ListenerList<IChangeSetChangeListener>(ListenerList.IDENTITY);
+ private Set<ChangeSet> sets;
+ private boolean initializing;
+
+ /**
+ * Return the list of listeners registered with this change set manager.
+ * @return the list of listeners registered with this change set manager
+ */
+ protected Object[] getListeners() {
+ return listeners.getListeners();
+ }
+
+ /**
+ * Method that can be invoked by subclasses when the name of
+ * a managed change set changes.
+ * @param set the set whose title has changed
+ */
+ protected void fireNameChangedEvent(final ChangeSet set) {
+ if (initializing)
+ return;
+ if (contains(set)) {
+ Object[] listeners = getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ final IChangeSetChangeListener listener = (IChangeSetChangeListener)listeners[i];
+ SafeRunner.run(new ISafeRunnable() {
+ @Override
+ public void handleException(Throwable exception) {
+ // Exceptions are logged by the platform
+ }
+ @Override
+ public void run() throws Exception {
+ listener.nameChanged(set);
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Method which allows subclasses to notify listeners that the default
+ * set has changed.
+ * @param oldSet the previous default
+ * @param defaultSet the new default
+ */
+ protected void fireDefaultChangedEvent(final ChangeSet oldSet, final ChangeSet defaultSet) {
+ if (initializing)
+ return;
+ Object[] listeners = getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ final IChangeSetChangeListener listener = (IChangeSetChangeListener)listeners[i];
+ SafeRunner.run(new ISafeRunnable() {
+ @Override
+ public void handleException(Throwable exception) {
+ // Exceptions are logged by the platform
+ }
+ @Override
+ public void run() throws Exception {
+ listener.defaultSetChanged(oldSet, defaultSet);
+ }
+ });
+ }
+ }
+
+ /**
+ * Add the set to the list of active sets.
+ * @param set the set to be added
+ */
+ public void add(final ChangeSet set) {
+ if (!contains(set)) {
+ internalGetSets().add(set);
+ handleSetAdded(set);
+ }
+ }
+
+ /**
+ * Handle the set addition by notifying listeners.
+ * @param set the added set
+ */
+ protected void handleSetAdded(final ChangeSet set) {
+ if (initializing)
+ return;
+ Object[] listeners = getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ final IChangeSetChangeListener listener = (IChangeSetChangeListener)listeners[i];
+ SafeRunner.run(new ISafeRunnable() {
+ @Override
+ public void handleException(Throwable exception) {
+ // Exceptions are logged by the platform
+ }
+ @Override
+ public void run() throws Exception {
+ listener.setAdded(set);
+ }
+ });
+ }
+ }
+
+ /**
+ * Remove the set from the list of active sets.
+ * @param set the set to be removed
+ */
+ public void remove(final ChangeSet set) {
+ if (contains(set)) {
+ internalGetSets().remove(set);
+ handleSetRemoved(set);
+ }
+ }
+
+ /**
+ * Handle the set removal by notifying listeners.
+ * @param set the removed set
+ */
+ protected void handleSetRemoved(final ChangeSet set) {
+ if (initializing)
+ return;
+ Object[] listeners = getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ final IChangeSetChangeListener listener = (IChangeSetChangeListener)listeners[i];
+ SafeRunner.run(new ISafeRunnable() {
+ @Override
+ public void handleException(Throwable exception) {
+ // Exceptions are logged by the platform
+ }
+ @Override
+ public void run() throws Exception {
+ listener.setRemoved(set);
+ }
+ });
+ }
+ }
+
+ /**
+ * Return whether the manager contains the given commit set
+ * @param set the commit set being tested
+ * @return whether the set is contained in the manager's list of active sets
+ */
+ public boolean contains(ChangeSet set) {
+ return internalGetSets().contains(set);
+ }
+
+ /**
+ * Add the listener to the set of registered listeners.
+ * @param listener the listener to be added
+ */
+ public void addListener(IChangeSetChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Remove the listener from the set of registered listeners.
+ * @param listener the listener to remove
+ */
+ public void removeListener(IChangeSetChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Return the list of active commit sets.
+ * @return the list of active commit sets
+ */
+ public ChangeSet[] getSets() {
+ Set<ChangeSet> sets = internalGetSets();
+ return sets.toArray(new ChangeSet[sets.size()]);
+ }
+
+ /**
+ * Dispose of any resources maintained by the manager
+ */
+ public void dispose() {
+ // Nothing to do
+ }
+
+ /**
+ * Fire resource change notifications to the listeners.
+ * @param changeSet
+ * @param allAffectedResources
+ */
+ protected void fireResourcesChangedEvent(final ChangeSet changeSet, final IPath[] allAffectedResources) {
+ if (initializing)
+ return;
+ Object[] listeners = getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ final IChangeSetChangeListener listener = (IChangeSetChangeListener)listeners[i];
+ SafeRunner.run(new ISafeRunnable() {
+ @Override
+ public void handleException(Throwable exception) {
+ // Exceptions are logged by the platform
+ }
+ @Override
+ public void run() throws Exception {
+ listener.resourcesChanged(changeSet, allAffectedResources);
+ }
+ });
+ }
+ }
+
+ private Set<ChangeSet> internalGetSets() {
+ if (sets == null) {
+ sets = Collections.synchronizedSet(new HashSet<>());
+ try {
+ initializing = true;
+ initializeSets();
+ } finally {
+ initializing = false;
+ }
+ }
+ return sets;
+ }
+
+ /**
+ * Initialize the sets contained in this manager.
+ * This method is called the first time the sets are accessed.
+ */
+ protected abstract void initializeSets();
+
+ public boolean isInitialized() {
+ return sets != null;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/CheckedInChangeSet.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/CheckedInChangeSet.java
new file mode 100644
index 000000000..bbabf4a6b
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/CheckedInChangeSet.java
@@ -0,0 +1,159 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.Date;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.team.core.synchronize.SyncInfo;
+import org.eclipse.team.core.synchronize.SyncInfoTree;
+
+/**
+ * A checked-in change set represents a group of resource
+ * changes that were previously checked into a repository
+ * as a single logical change.
+ * <p>
+ * A previously checked-in set of changes may not apply directly
+ * to the local versions of the resources involved. However,
+ * a <code>SyncInfo</code> is still used to represent each change.
+ * The base and remote slots of the <code>SyncInfo</code> identify
+ * the state before and after the resources were checked-in.
+ * @since 3.1
+ */
+public abstract class CheckedInChangeSet extends ChangeSet {
+
+ private final SyncInfoTree set = new SyncInfoTree();
+
+ public abstract String getAuthor();
+
+ public abstract Date getDate();
+
+ /**
+ * Return the SyncInfoSet that contains the resources that belong to this change set.
+ * @return the SyncInfoSet that contains the resources that belong to this change set
+ */
+ public SyncInfoTree getSyncInfoSet() {
+ return set;
+ }
+
+ /**
+ * Return the resources that are contained in this set.
+ * @return the resources that are contained in this set
+ */
+ @Override
+ public IResource[] getResources() {
+ return set.getResources();
+ }
+
+ /**
+ * Return whether the set contains any files.
+ * @return whether the set contains any files
+ */
+ @Override
+ public boolean isEmpty() {
+ return set.isEmpty();
+ }
+
+ /**
+ * Return true if the given file is included in this set.
+ * @param local a local file
+ * @return true if the given file is included in this set
+ */
+ @Override
+ public boolean contains(IResource local) {
+ return set.getSyncInfo(local) != null;
+ }
+
+ /**
+ * Add the resource to this set if it is modified
+ * w.r.t. the subscriber.
+ * @param info
+ */
+ public void add(SyncInfo info) {
+ if (isValidChange(info)) {
+ set.add(info);
+ }
+ }
+
+ /**
+ * Return whether the given sync info is a valid change
+ * and can be included in this set. This method is used
+ * by the <code>add</code> method to filter set additions.
+ * @param info a sync info
+ * @return whether the sync info is a valid member of this set
+ */
+ protected boolean isValidChange(SyncInfo info) {
+ return (info != null);
+ }
+
+ /**
+ * Add the resources to this set if they are modified
+ * w.r.t. the subscriber.
+ * @param infos the resources to be added.
+ */
+ public void add(SyncInfo[] infos) {
+ try {
+ set.beginInput();
+ for (int i = 0; i < infos.length; i++) {
+ SyncInfo info = infos[i];
+ add(info);
+ }
+ } finally {
+ set.endInput(null);
+ }
+ }
+
+ /**
+ * Remove the resource from the set.
+ * @param resource the resource to be removed
+ */
+ @Override
+ public void remove(IResource resource) {
+ if (contains(resource)) {
+ set.remove(resource);
+ }
+ }
+
+ /**
+ * Remove the resources from the set.
+ * @param resources the resources to be removed
+ */
+ @Override
+ public void remove(IResource[] resources) {
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ remove(resource);
+ }
+ }
+
+ /**
+ * Remove the resource and it's descendants to the given depth.
+ * @param resource the resource to be removed
+ * @param depth the depth of the removal (one of
+ * <code>IResource.DEPTH_ZERO, IResource.DEPTH_ONE, IResource.DEPTH_INFINITE)</code>
+ */
+ @Override
+ public void rootRemoved(IResource resource, int depth) {
+ SyncInfo[] infos = set.getSyncInfos(resource, depth);
+ if (infos.length > 0) {
+ IResource[] resources = new IResource[infos.length];
+ for (int i = 0; i < resources.length; i++) {
+ resources[i] = infos[i].getLocal();
+ }
+ set.removeAll(resources);
+ }
+ }
+
+ @Override
+ public boolean containsChildren(IResource resource, int depth) {
+ return set.getSyncInfos(resource, depth).length > 0;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ContentComparator.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ContentComparator.java
new file mode 100644
index 000000000..fb0337dd1
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ContentComparator.java
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+
+/**
+ * This is an internal class that is used by the
+ * {@link org.eclipse.team.core.synchronize.SyncInfoFilter.ContentComparisonSyncInfoFilter}
+ * and {@link ContentComparisonDiffFilter} to compare the contents of the local
+ * and remote resources.
+ */
+public class ContentComparator extends AbstractContentComparator{
+
+ public ContentComparator(boolean ignoreWhitespace) {
+ super(ignoreWhitespace);
+ }
+
+ /**
+ * Returns <code>true</code> if both input streams byte contents is
+ * identical.
+ *
+ * @param is1
+ * first input to contents compare
+ * @param is2
+ * second input to contents compare
+ * @return <code>true</code> if content is equal
+ */
+ @Override
+ protected boolean contentsEqual(IProgressMonitor monitor, InputStream is1,
+ InputStream is2, boolean ignoreWhitespace) {
+ try {
+ if (is1 == is2)
+ return true;
+ // no byte contents
+ if (is1 == null && is2 == null)
+ return true;
+ // only one has contents
+ if (is1 == null || is2 == null)
+ return false;
+
+ while (true) {
+ int c1 = is1.read();
+ while (shouldIgnoreWhitespace() && isWhitespace(c1))
+ c1 = is1.read();
+ int c2 = is2.read();
+ while (shouldIgnoreWhitespace() && isWhitespace(c2))
+ c2 = is2.read();
+ if (c1 == -1 && c2 == -1)
+ return true;
+ if (c1 != c2)
+ break;
+ }
+ } catch (IOException ex) {
+ } finally {
+ try {
+ try {
+ if (is1 != null) {
+ is1.close();
+ }
+ } finally {
+ if (is2 != null) {
+ is2.close();
+ }
+ }
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ return false;
+ }
+
+ private boolean isWhitespace(int c) {
+ if (c == -1)
+ return false;
+ return Character.isWhitespace((char) c);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ContentComparisonDiffFilter.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ContentComparisonDiffFilter.java
new file mode 100644
index 000000000..a838f89b0
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ContentComparisonDiffFilter.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.diff.DiffFilter;
+import org.eclipse.team.core.diff.IDiff;
+import org.eclipse.team.core.history.IFileRevision;
+import org.eclipse.team.core.mapping.provider.ResourceDiffTree;
+import org.eclipse.team.internal.core.mapping.SyncInfoToDiffConverter;
+
+public class ContentComparisonDiffFilter extends DiffFilter {
+ ContentComparator criteria = new ContentComparator(false);
+
+ /**
+ * Create a filter that does not ignore whitespace.
+ */
+ public ContentComparisonDiffFilter() {
+ this(false);
+ }
+ /**
+ * Create a filter and configure how whitespace is handled.
+ * @param ignoreWhitespace whether whitespace should be ignored
+ */
+ public ContentComparisonDiffFilter(boolean ignoreWhitespace) {
+ criteria = new ContentComparator(ignoreWhitespace);
+ }
+
+ /**
+ * Compare the contents of the local file and its variant.
+ * This is used by the <code>select</code> method to compare the
+ * contents of two non-null files.
+ * @param local a local file
+ * @param remote a resource variant of the file
+ * @param monitor a progress monitor
+ * @return whether the contents of the two files are equal
+ */
+ public boolean compareContents(IFile local, IFileRevision remote, IProgressMonitor monitor) {
+ Assert.isNotNull(local);
+ Assert.isNotNull(remote);
+ return criteria.compare(local, remote, monitor);
+ }
+
+ @Override
+ public boolean select(IDiff diff, IProgressMonitor monitor) {
+ IFileRevision remote = SyncInfoToDiffConverter.getRemote(diff);
+ IResource local = ResourceDiffTree.getResourceFor(diff);
+ if (local == null) return true;
+ if (local.getType() != IResource.FILE) return false;
+ if (remote == null) return !local.exists();
+ if (!local.exists()) return false;
+ return compareContents((IFile)local, remote, monitor);
+ }
+} \ No newline at end of file
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DescendantResourceVariantByteStore.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DescendantResourceVariantByteStore.java
new file mode 100644
index 000000000..aeeab75e1
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DescendantResourceVariantByteStore.java
@@ -0,0 +1,173 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRunnable;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.variants.*;
+
+/**
+ * A <code>ResourceVariantByteStore</code> that optimizes the memory footprint
+ * of a remote resource variant tree by only storing those bytes that differ
+ * from a base resource variant tree. This class should only be used for cases
+ * where the base and remote are on the same line-of-descent. For example, when
+ * the remote tree represents the current state of a branch and the base
+ * represents the state of the same branch when the local workspace as last
+ * refreshed.
+ * <p>
+ * This class also contains the logic that allows subclasses to determine if
+ * bytes stored in the remote tree are on a different line-of-descent than the
+ * base. This is necessary because it is possible for the base tree to change in
+ * ways that invalidate the stored remote variants. For example, if the local
+ * resources are moved from the main trunk to a branch, any cached remote
+ * resource variants would be stale.
+ *
+ * @since 3.0
+ */
+public abstract class DescendantResourceVariantByteStore extends ResourceVariantByteStore {
+
+ ResourceVariantByteStore baseStore, remoteStore;
+
+ public DescendantResourceVariantByteStore(ResourceVariantByteStore baseCache, ResourceVariantByteStore remoteCache) {
+ this.baseStore = baseCache;
+ this.remoteStore = remoteCache;
+ }
+
+ /**
+ * This method will dispose the remote cache but not the base cache.
+ * @see org.eclipse.team.core.variants.ResourceVariantByteStore#dispose()
+ */
+ @Override
+ public void dispose() {
+ remoteStore.dispose();
+ }
+
+ @Override
+ public byte[] getBytes(IResource resource) throws TeamException {
+ byte[] remoteBytes = remoteStore.getBytes(resource);
+ byte[] baseBytes = baseStore.getBytes(resource);
+ if (baseBytes == null) {
+ // There is no base so use the remote bytes
+ return remoteBytes;
+ }
+ if (remoteBytes == null) {
+ if (isVariantKnown(resource)) {
+ // The remote is known to not exist
+ // TODO: The check for NO_REMOTE does not take into consideration the line-of-descent
+ return remoteBytes;
+ } else {
+ // The remote was either never queried or was the same as the base.
+ // In either of these cases, the base bytes are used.
+ return baseBytes;
+ }
+ }
+ if (isDescendant(resource, baseBytes, remoteBytes)) {
+ // Only use the remote bytes if they are later on the same line-of-descent as the base
+ return remoteBytes;
+ }
+ // Use the base sbytes since the remote bytes must be stale (i.e. are
+ // not on the same line-of-descent
+ return baseBytes;
+ }
+
+ @Override
+ public boolean setBytes(IResource resource, byte[] bytes) throws TeamException {
+ byte[] baseBytes = baseStore.getBytes(resource);
+ if (baseBytes != null && equals(baseBytes, bytes)) {
+ // Remove the existing bytes so the base will be used (thus saving space)
+ return remoteStore.flushBytes(resource, IResource.DEPTH_ZERO);
+ } else {
+ return remoteStore.setBytes(resource, bytes);
+ }
+ }
+
+ @Override
+ public boolean flushBytes(IResource resource, int depth) throws TeamException {
+ return remoteStore.flushBytes(resource, depth);
+ }
+
+ /**
+ * Return <code>true</code> if the variant associated with the given local
+ * resource has been cached. This method is useful for those cases when
+ * there are no bytes for a resource variant and the client wants to
+ * know if this means that the remote does exist (i.e. this method returns
+ * <code>true</code>) or the remote has not been fetched (i.e. this method returns
+ * <code>false</code>).
+ * @param resource the local resource
+ * @return <code>true</code> if the variant associated with the given local
+ * resource has been cached.
+ * @throws TeamException
+ */
+ public abstract boolean isVariantKnown(IResource resource) throws TeamException;
+
+ /**
+ * This method indicates whether the remote bytes are a later revision or version
+ * on the same line-of-descent as the base. A line of descent may be a branch or a fork
+ * (depending on the terminology used by the versioing server). If this method returns
+ * <code>false</code> then the remote bytes will be ignored by this tree.
+ * @param resource the local resource
+ * @param baseBytes the base bytes for the local resoource
+ * @param remoteBytes the remote bytes for the local resoource
+ * @return whether the remote bytes are later on the same line-of-descent as the base bytes
+ */
+ protected abstract boolean isDescendant(IResource resource, byte[] baseBytes, byte[] remoteBytes) throws TeamException;
+
+ @Override
+ public boolean deleteBytes(IResource resource) throws TeamException {
+ return remoteStore.deleteBytes(resource);
+ }
+
+ /**
+ * Return the base tree from which the remote is descendant.
+ * @return Returns the base tree.
+ */
+ protected ResourceVariantByteStore getBaseStore() {
+ return baseStore;
+ }
+
+ /**
+ * Return the remote tree which contains bytes only for the resource variants
+ * that differ from those in the base tree.
+ * @return Returns the remote tree.
+ */
+ protected ResourceVariantByteStore getRemoteStore() {
+ return remoteStore;
+ }
+
+ @Override
+ public IResource[] members(IResource resource) throws TeamException {
+ IResource[] remoteMembers = getRemoteStore().members(resource);
+ IResource[] baseMembers = getBaseStore().members(resource);
+ Set<IResource> members = new HashSet<>();
+ for (int i = 0; i < remoteMembers.length; i++) {
+ members.add(remoteMembers[i]);
+ }
+ for (int i = 0; i < baseMembers.length; i++) {
+ IResource member = baseMembers[i];
+ // Add the base only if the remote does not know about it
+ // (i.e. hasn't marked it as deleted
+ if (!isVariantKnown(member)) {
+ members.add(member);
+ }
+ }
+ return members.toArray(new IResource[members.size()]);
+ }
+
+ @Override
+ public void run(IResource root, IWorkspaceRunnable runnable, IProgressMonitor monitor) throws TeamException {
+ remoteStore.run(root, runnable, monitor);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DiffChangeSet.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DiffChangeSet.java
new file mode 100644
index 000000000..fa5c9f4a1
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DiffChangeSet.java
@@ -0,0 +1,184 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.team.core.diff.IDiff;
+import org.eclipse.team.core.mapping.IResourceDiffTree;
+import org.eclipse.team.core.mapping.provider.ResourceDiffTree;
+
+public class DiffChangeSet extends ChangeSet {
+
+ private final ResourceDiffTree tree = new ResourceDiffTree();
+
+ public DiffChangeSet() {
+ super();
+ }
+
+ public DiffChangeSet(String name) {
+ super(name);
+ }
+
+ /**
+ * Return the diff tree that contains the resources that belong to this change set.
+ * @return the diff tree that contains the resources that belong to this change set
+ */
+ public IResourceDiffTree getDiffTree() {
+ return tree;
+ }
+
+ protected ResourceDiffTree internalGetDiffTree() {
+ return tree;
+ }
+
+ /**
+ * Return the resources that are contained in this set.
+ * @return the resources that are contained in this set
+ */
+ @Override
+ public IResource[] getResources() {
+ return tree.getAffectedResources();
+ }
+
+ /**
+ * Return whether the set contains any files.
+ * @return whether the set contains any files
+ */
+ @Override
+ public boolean isEmpty() {
+ return tree.isEmpty();
+ }
+
+ /**
+ * Return true if the given file is included in this set.
+ * @param local a local file
+ * @return true if the given file is included in this set
+ */
+ @Override
+ public boolean contains(IResource local) {
+ return tree.getDiff(local) != null;
+ }
+
+ /**
+ * Add the resource to this set if it is modified
+ * w.r.t. the subscriber.
+ * @param diff the diff to be added
+ */
+ public void add(IDiff diff) {
+ if (isValidChange(diff)) {
+ tree.add(diff);
+ }
+ }
+
+ /**
+ * Return whether the given sync info is a valid change
+ * and can be included in this set. This method is used
+ * by the <code>add</code> method to filter set additions.
+ * @param diff a diff
+ * @return whether the sync info is a valid member of this set
+ */
+ protected boolean isValidChange(IDiff diff) {
+ return (diff != null);
+ }
+
+ /**
+ * Add the resources to this set if they are modified
+ * w.r.t. the subscriber.
+ * @param diffs the resources to be added.
+ */
+ public void add(IDiff[] diffs) {
+ try {
+ tree.beginInput();
+ for (int i = 0; i < diffs.length; i++) {
+ IDiff diff = diffs[i];
+ add(diff);
+ }
+ } finally {
+ tree.endInput(null);
+ }
+ }
+
+ /**
+ * Remove the resource from the set.
+ * @param resource the resource to be removed
+ */
+ @Override
+ public void remove(IResource resource) {
+ if (contains(resource)) {
+ tree.remove(resource);
+ }
+ }
+
+ /**
+ * Remove the resource and it's descendants to the given depth.
+ * @param resource the resource to be removed
+ * @param depth the depth of the removal (one of
+ * <code>IResource.DEPTH_ZERO, IResource.DEPTH_ONE, IResource.DEPTH_INFINITE)</code>
+ */
+ @Override
+ public void rootRemoved(IResource resource, int depth) {
+ IDiff[] diffs = tree.getDiffs(resource, depth);
+ if (diffs.length > 0) {
+ try {
+ tree.beginInput();
+ for (int i = 0; i < diffs.length; i++) {
+ IDiff diff = diffs[i];
+ IResource r = tree.getResource(diff);
+ if (r != null)
+ tree.remove(r);
+ }
+ } finally {
+ tree.endInput(null);
+ }
+ }
+ }
+
+ public boolean contains(IPath path) {
+ return getDiffTree().getDiff(path) != null;
+ }
+
+ @Override
+ public boolean containsChildren(IResource resource, int depth) {
+ return getDiffTree().getDiffs(resource, depth).length > 0;
+ }
+
+ public void remove(IPath[] paths) {
+ try {
+ tree.beginInput();
+ for (int i = 0; i < paths.length; i++) {
+ IPath path = paths[i];
+ tree.remove(path);
+ }
+ } finally {
+ tree.endInput(null);
+ }
+ }
+
+ @Override
+ public void remove(IResource[] resources) {
+ try {
+ tree.beginInput();
+ for (int i = 0; i < resources.length; i++) {
+ IResource resource = resources[i];
+ tree.remove(resource);
+ }
+ } finally {
+ tree.endInput(null);
+ }
+ }
+
+ @Override
+ public String getComment() {
+ return null;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DiffTreeStatistics.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DiffTreeStatistics.java
new file mode 100644
index 000000000..757f6f131
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/DiffTreeStatistics.java
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * Copyright (c) 2006, 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.team.core.diff.IDiff;
+import org.eclipse.team.core.diff.IThreeWayDiff;
+import org.eclipse.team.core.synchronize.SyncInfo;
+
+public class DiffTreeStatistics {
+ /**
+ * {Integer sync kind -> Long number of infos with that sync kind in this sync set}
+ */
+ protected Map<Integer, Long> stats = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Count this sync state.
+ * @param state the state
+ */
+ public void add(int state) {
+ // update statistics
+ Long count = stats.get(Integer.valueOf(state));
+ if(count == null) {
+ count = Long.valueOf(0);
+ }
+ stats.put(Integer.valueOf(state), Long.valueOf(count.longValue() + 1));
+ }
+
+ /**
+ * Remove this sync kind.
+ * @param state the info type to remove
+ */
+ public void remove(int state) {
+ // update stats
+ Integer kind = Integer.valueOf(state);
+ Long count = stats.get(kind);
+ if(count == null) {
+ // error condition, shouldn't be removing if we haven't added yet
+ // programmer error calling remove before add.
+ } else {
+ long newCount = count.intValue() - 1;
+ if(newCount > 0) {
+ stats.put(kind, Long.valueOf(newCount));
+ } else {
+ stats.remove(kind);
+ }
+ }
+ }
+
+ /**
+ * Return the count of sync infos for the specified sync kind. A mask can be used to accumulate
+ * counts for specific directions or change types.
+ * To return the number of outgoing changes:
+ * long outgoingChanges = stats.countFor(SyncInfo.OUTGOING, SyncInfo.DIRECTION_MASK);
+ *
+ * @param state the sync kind for which to return the count
+ * @param mask the mask applied to the stored sync kind
+ * @return the number of sync info types added for the specific kind
+ */
+ public long countFor(int state, int mask) {
+ if(mask == 0) {
+ Long count = stats.get(Integer.valueOf(state));
+ return count == null ? 0 : count.longValue();
+ } else {
+ Set keySet = stats.keySet();
+ long count = 0;
+ synchronized (stats) {
+ Iterator it = keySet.iterator();
+ while (it.hasNext()) {
+ Integer key = (Integer) it.next();
+ if((key.intValue() & mask) == state) {
+ count += stats.get(key).intValue();
+ }
+ }
+ }
+ return count;
+ }
+ }
+
+ /**
+ * Clear the statistics counts. All calls to countFor() will return 0 until new
+ * sync infos are added.
+ */
+ public void clear() {
+ stats.clear();
+ }
+
+ /**
+ * For debugging
+ */
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder();
+ Iterator it = stats.keySet().iterator();
+ while (it.hasNext()) {
+ Integer kind = (Integer) it.next();
+ out.append(SyncInfo.kindToString(kind.intValue()) + ": " + stats.get(kind) + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ return out.toString();
+ }
+
+ public void add(IDiff delta) {
+ int state = getState(delta);
+ add(state);
+ }
+
+ public void remove(IDiff delta) {
+ int state = getState(delta);
+ remove(state);
+ }
+
+ private int getState(IDiff delta) {
+ int state = delta.getKind();
+ if (delta instanceof IThreeWayDiff) {
+ IThreeWayDiff twd = (IThreeWayDiff) delta;
+ state |= twd.getDirection();
+ }
+ return state;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/IChangeSetChangeListener.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/IChangeSetChangeListener.java
new file mode 100644
index 000000000..9d9a9d0fc
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/IChangeSetChangeListener.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2006 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.internal.core.subscribers;
+
+import org.eclipse.core.runtime.IPath;
+
+
+/**
+ * Interface for registering change set change listeners with
+ * the change set manager.
+ */
+public interface IChangeSetChangeListener {
+
+ /**
+ * The given set has been added to the set manager.
+ * @param set the added set
+ */
+ void setAdded(ChangeSet set);
+
+ /**
+ * The default change set has change to be the given set.
+ * All new modifications will be placed in the default
+ * set.
+ * @param previousDefault
+ * @param set the default set
+ */
+ void defaultSetChanged(ChangeSet previousDefault, ChangeSet set);
+
+ /**
+ * The given set has been removed from the set manager.
+ * @param set the removed set
+ */
+ void setRemoved(ChangeSet set);
+
+ /**
+ * The title of the given set has changed.
+ * @param set the set whose title changed
+ */
+ void nameChanged(ChangeSet set);
+
+ /**
+ * The state of the given resources have change with respect to the
+ * given set. This means that the resource have either been added
+ * or removed from the set. Callers can use the resources contained
+ * in the set to determine if each resource is an addition or removal.
+ * @param set the set that has changed
+ * @param paths the paths of the resources whose containment state has changed w.r.t the set
+ */
+ void resourcesChanged(ChangeSet set, IPath[] paths);
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/RootResourceSynchronizationScope.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/RootResourceSynchronizationScope.java
new file mode 100644
index 000000000..c193f3bf6
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/RootResourceSynchronizationScope.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.resources.mapping.*;
+import org.eclipse.team.core.mapping.ISynchronizationScope;
+import org.eclipse.team.internal.core.mapping.AbstractResourceMappingScope;
+
+/**
+ * A synchronization scope for a set of resources.
+ */
+public class RootResourceSynchronizationScope extends AbstractResourceMappingScope {
+
+ private IResource[] roots;
+
+ public RootResourceSynchronizationScope(IResource[] roots) {
+ this.roots = roots;
+ }
+
+ @Override
+ public ResourceTraversal[] getTraversals() {
+ return new ResourceTraversal[] {new ResourceTraversal(roots, IResource.DEPTH_INFINITE, IResource.NONE)};
+ }
+
+ /**
+ * Set the traversal of this scope to a single traversal
+ * of infinite depth on the given resources.
+ * @param roots the new roots of the scope
+ */
+ public void setRoots(IResource[] roots) {
+ this.roots = roots;
+ fireTraversalsChangedEvent(getTraversals(), getMappings());
+ }
+
+ @Override
+ public ResourceMapping[] getInputMappings() {
+ return getMappings();
+ }
+
+ @Override
+ public ISynchronizationScope asInputScope() {
+ return this;
+ }
+
+ @Override
+ public ResourceMapping[] getMappings() {
+ List<ResourceMapping> result = new ArrayList<>();
+ for (int i = 0; i < roots.length; i++) {
+ IResource resource = roots[i];
+ Object o = resource.getAdapter(ResourceMapping.class);
+ if (o instanceof ResourceMapping) {
+ result.add((ResourceMapping) o);
+ }
+ }
+ return result.toArray(new ResourceMapping[result.size()]);
+ }
+
+ @Override
+ public ResourceTraversal[] getTraversals(ResourceMapping mapping) {
+ Object object = mapping.getModelObject();
+ if (object instanceof IResource) {
+ IResource resource = (IResource) object;
+ return new ResourceTraversal[] {new ResourceTraversal(new IResource[] { resource }, IResource.DEPTH_INFINITE, IResource.NONE)};
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasAdditionalMappings() {
+ return false;
+ }
+
+ @Override
+ public boolean hasAdditonalResources() {
+ return false;
+ }
+
+ @Override
+ public IProject[] getProjects() {
+ return ResourcesPlugin.getWorkspace().getRoot().getProjects();
+ }
+
+ @Override
+ public ResourceMappingContext getContext() {
+ return ResourceMappingContext.LOCAL_CONTEXT;
+ }
+
+ @Override
+ public void refresh(ResourceMapping[] mappings) {
+ // Not supported
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberChangeSetManager.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberChangeSetManager.java
new file mode 100644
index 000000000..a499a32b2
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberChangeSetManager.java
@@ -0,0 +1,355 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.preferences.InstanceScope;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.diff.IDiff;
+import org.eclipse.team.core.mapping.provider.ResourceDiffTree;
+import org.eclipse.team.core.subscribers.Subscriber;
+import org.eclipse.team.internal.core.*;
+import org.osgi.service.prefs.Preferences;
+
+/**
+ * This class manages the active change sets associated with a subscriber.
+ */
+public class SubscriberChangeSetManager extends ActiveChangeSetManager {
+
+ private static final String PREF_CHANGE_SETS = "changeSets"; //$NON-NLS-1$
+
+ private static final int RESOURCE_REMOVAL = 1;
+ private static final int RESOURCE_CHANGE = 2;
+
+ private EventHandler handler;
+ private ResourceCollector collector;
+
+ /*
+ * Background event handler for serializing and batching change set changes
+ */
+ private class EventHandler extends BackgroundEventHandler {
+
+ private List<Event> dispatchEvents = new ArrayList<>();
+
+ protected EventHandler(String jobName, String errorTitle) {
+ super(jobName, errorTitle);
+ }
+
+ @Override
+ protected void processEvent(Event event, IProgressMonitor monitor) throws CoreException {
+ // Handle everything in the dispatch
+ if (isShutdown())
+ throw new OperationCanceledException();
+ dispatchEvents.add(event);
+ }
+
+ @Override
+ protected boolean doDispatchEvents(IProgressMonitor monitor) throws TeamException {
+ if (dispatchEvents.isEmpty()) {
+ return false;
+ }
+ if (isShutdown())
+ throw new OperationCanceledException();
+ ResourceDiffTree[] locked = null;
+ try {
+ locked = beginDispath();
+ for (Iterator iter = dispatchEvents.iterator(); iter.hasNext();) {
+ Event event = (Event) iter.next();
+ switch (event.getType()) {
+ case RESOURCE_REMOVAL:
+ handleRemove(event.getResource());
+ break;
+ case RESOURCE_CHANGE:
+ handleChange(event.getResource(), ((ResourceEvent)event).getDepth());
+ break;
+ default:
+ break;
+ }
+ if (isShutdown())
+ throw new OperationCanceledException();
+ }
+ } catch (CoreException e) {
+ throw TeamException.asTeamException(e);
+ } finally {
+ try {
+ endDispatch(locked, monitor);
+ } finally {
+ dispatchEvents.clear();
+ }
+ }
+ return true;
+ }
+
+ /*
+ * Begin input on all the sets and return the sync sets that were
+ * locked. If this method throws an exception then the client
+ * can assume that no sets were locked
+ */
+ private ResourceDiffTree[] beginDispath() {
+ ChangeSet[] sets = getSets();
+ List<ResourceDiffTree> lockedSets = new ArrayList<>();
+ try {
+ for (int i = 0; i < sets.length; i++) {
+ ActiveChangeSet set = (ActiveChangeSet)sets[i];
+ ResourceDiffTree tree = set.internalGetDiffTree();
+ lockedSets.add(tree);
+ tree.beginInput();
+ }
+ return lockedSets.toArray(new ResourceDiffTree[lockedSets.size()]);
+ } catch (RuntimeException e) {
+ try {
+ for (Iterator iter = lockedSets.iterator(); iter.hasNext();) {
+ ResourceDiffTree tree = (ResourceDiffTree) iter.next();
+ try {
+ tree.endInput(null);
+ } catch (Throwable e1) {
+ // Ignore so that original exception is not masked
+ }
+ }
+ } catch (Throwable e1) {
+ // Ignore so that original exception is not masked
+ }
+ throw e;
+ }
+ }
+
+ private void endDispatch(ResourceDiffTree[] locked, IProgressMonitor monitor) {
+ if (locked == null) {
+ // The begin failed so there's nothing to unlock
+ return;
+ }
+ monitor.beginTask(null, 100 * locked.length);
+ for (int i = 0; i < locked.length; i++) {
+ ResourceDiffTree tree = locked[i];
+ try {
+ tree.endInput(Policy.subMonitorFor(monitor, 100));
+ } catch (RuntimeException e) {
+ // Don't worry about ending every set if an error occurs.
+ // Instead, log the error and suggest a restart.
+ TeamPlugin.log(IStatus.ERROR, Messages.SubscriberChangeSetCollector_0, e);
+ throw e;
+ }
+ }
+ monitor.done();
+ }
+
+ @Override
+ protected synchronized void queueEvent(Event event, boolean front) {
+ // Override to allow access from enclosing class
+ super.queueEvent(event, front);
+ }
+
+ /*
+ * Handle the removal
+ */
+ private void handleRemove(IResource resource) {
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ChangeSet set = sets[i];
+ // This will remove any descendants from the set and callback to
+ // resourcesChanged which will batch changes
+ if (!set.isEmpty()) {
+ set.rootRemoved(resource, IResource.DEPTH_INFINITE);
+ if (set.isEmpty()) {
+ remove(set);
+ }
+ }
+ }
+ }
+
+ /*
+ * Handle the change
+ */
+ private void handleChange(IResource resource, int depth) throws CoreException {
+ IDiff diff = getDiff(resource);
+ if (isModified(diff)) {
+ ActiveChangeSet[] containingSets = getContainingSets(resource);
+ if (containingSets.length == 0) {
+ // Consider for inclusion in the default set
+ // if the resource is not already a member of another set
+ if (getDefaultSet() != null) {
+ getDefaultSet().add(diff);
+ }
+ } else {
+ for (int i = 0; i < containingSets.length; i++) {
+ ActiveChangeSet set = containingSets[i];
+ // Update the sync info in the set
+ set.add(diff);
+ }
+ }
+ } else {
+ removeFromAllSets(resource);
+ }
+ if (depth != IResource.DEPTH_ZERO) {
+ IResource[] members = getSubscriber().members(resource);
+ for (int i = 0; i < members.length; i++) {
+ IResource member = members[i];
+ handleChange(member, depth == IResource.DEPTH_ONE ? IResource.DEPTH_ZERO : IResource.DEPTH_INFINITE);
+ }
+ }
+ }
+
+ private void removeFromAllSets(IResource resource) {
+ List<ChangeSet> toRemove = new ArrayList<>();
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ChangeSet set = sets[i];
+ if (set.contains(resource)) {
+ set.remove(resource);
+ if (set.isEmpty()) {
+ toRemove.add(set);
+ }
+ }
+ }
+ for (Object element : toRemove) {
+ ActiveChangeSet set = (ActiveChangeSet) element;
+ remove(set);
+ }
+ }
+
+ private ActiveChangeSet[] getContainingSets(IResource resource) {
+ Set<ActiveChangeSet> result = new HashSet<>();
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ChangeSet set = sets[i];
+ if (set.contains(resource)) {
+ result.add((ActiveChangeSet) set);
+ }
+ }
+ return result.toArray(new ActiveChangeSet[result.size()]);
+ }
+ }
+
+ private class ResourceCollector extends SubscriberResourceCollector {
+
+ public ResourceCollector(Subscriber subscriber) {
+ super(subscriber);
+ }
+
+ @Override
+ protected void remove(IResource resource) {
+ if (handler != null)
+ handler.queueEvent(new BackgroundEventHandler.ResourceEvent(resource, RESOURCE_REMOVAL, IResource.DEPTH_INFINITE), false);
+ }
+
+ @Override
+ protected void change(IResource resource, int depth) {
+ if (handler != null)
+ handler.queueEvent(new BackgroundEventHandler.ResourceEvent(resource, RESOURCE_CHANGE, depth), false);
+ }
+
+ @Override
+ protected boolean hasMembers(IResource resource) {
+ return SubscriberChangeSetManager.this.hasMembers(resource);
+ }
+ }
+
+ public SubscriberChangeSetManager(Subscriber subscriber) {
+ collector = new ResourceCollector(subscriber);
+ handler = new EventHandler(NLS.bind(Messages.SubscriberChangeSetCollector_1, new String[] { subscriber.getName() }), NLS.bind(Messages.SubscriberChangeSetCollector_2, new String[] { subscriber.getName() })); //
+ }
+
+ @Override
+ protected void initializeSets() {
+ load(getPreferences());
+ }
+
+ public boolean hasMembers(IResource resource) {
+ ChangeSet[] sets = getSets();
+ for (int i = 0; i < sets.length; i++) {
+ ActiveChangeSet set = (ActiveChangeSet)sets[i];
+ if (set.getDiffTree().getChildren(resource.getFullPath()).length > 0)
+ return true;
+ }
+ if (getDefaultSet() != null)
+ return (getDefaultSet().getDiffTree().getChildren(resource.getFullPath()).length > 0);
+ return false;
+ }
+
+ /**
+ * Return the sync info for the given resource obtained
+ * from the subscriber.
+ * @param resource the resource
+ * @return the sync info for the resource
+ * @throws CoreException
+ */
+ @Override
+ public IDiff getDiff(IResource resource) throws CoreException {
+ Subscriber subscriber = getSubscriber();
+ return subscriber.getDiff(resource);
+ }
+
+ /**
+ * Return the subscriber associated with this collector.
+ * @return the subscriber associated with this collector
+ */
+ public Subscriber getSubscriber() {
+ return collector.getSubscriber();
+ }
+
+ @Override
+ public void dispose() {
+ handler.shutdown();
+ collector.dispose();
+ super.dispose();
+ save(getPreferences());
+ }
+
+ private Preferences getPreferences() {
+ return getParentPreferences().node(getSubscriberIdentifier());
+ }
+
+ private static Preferences getParentPreferences() {
+ return getTeamPreferences().node(PREF_CHANGE_SETS);
+ }
+
+ private static Preferences getTeamPreferences() {
+ return InstanceScope.INSTANCE.getNode(TeamPlugin.getPlugin().getBundle().getSymbolicName());
+ }
+
+ /**
+ * Return the id that will uniquely identify the subscriber across
+ * restarts.
+ * @return the id that will uniquely identify the subscriber across
+ */
+ protected String getSubscriberIdentifier() {
+ return getSubscriber().getName();
+ }
+
+ /**
+ * Wait until the collector is done processing any events.
+ * This method is for testing purposes only.
+ * @param monitor
+ */
+ public void waitUntilDone(IProgressMonitor monitor) {
+ monitor.worked(1);
+ // wait for the event handler to process changes.
+ while(handler.getEventHandlerJob().getState() != Job.NONE) {
+ monitor.worked(1);
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ }
+ Policy.checkCanceled(monitor);
+ }
+ monitor.worked(1);
+ }
+
+ @Override
+ protected String getName() {
+ return getSubscriber().getName();
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberDiffTreeEventHandler.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberDiffTreeEventHandler.java
new file mode 100644
index 000000000..745e0324d
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberDiffTreeEventHandler.java
@@ -0,0 +1,338 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.resources.mapping.ResourceTraversal;
+import org.eclipse.core.runtime.*;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.ITeamStatus;
+import org.eclipse.team.core.TeamStatus;
+import org.eclipse.team.core.diff.*;
+import org.eclipse.team.core.mapping.IResourceDiffTree;
+import org.eclipse.team.core.mapping.ISynchronizationScopeManager;
+import org.eclipse.team.core.mapping.provider.ResourceDiffTree;
+import org.eclipse.team.core.subscribers.Subscriber;
+import org.eclipse.team.internal.core.*;
+
+/**
+ * A subscriber event handler whose output is a diff tree
+ */
+public class SubscriberDiffTreeEventHandler extends SubscriberEventHandler {
+
+ // State constants for the event handler
+ private static final int STATE_NEW = 0;
+ public static final int STATE_STARTED = 1;
+ private static final int STATE_OK_TO_INITIALIZE = 3;
+ private static final int STATE_COLLECTING_CHANGES = 5;
+ private static final int STATE_SHUTDOWN = 8;
+
+ // state constants for exceptions
+ private static final int EXCEPTION_NONE = 0;
+ private static final int EXCEPTION_CANCELED = 1;
+ private static final int EXCEPTION_ERROR = 2;
+
+ private ResourceDiffTree tree;
+ private SubscriberDiffCollector collector;
+ private ISynchronizationScopeManager manager;
+ private Object family;
+ private DiffFilter filter;
+ private int state = STATE_NEW;
+ private int exceptionState = EXCEPTION_NONE;
+
+ /*
+ * An event used to represent a change in a diff
+ */
+ private class SubscriberDiffChangedEvent extends SubscriberEvent {
+ private final IDiff node;
+
+ public SubscriberDiffChangedEvent(IResource resource, int type, int depth, IDiff node) {
+ super(resource, type, depth);
+ this.node = node;
+ }
+ public IDiff getChangedNode() {
+ return node;
+ }
+ }
+
+ /*
+ * Collects resource and subscriber changes
+ */
+ private class SubscriberDiffCollector extends SubscriberResourceCollector {
+
+ public SubscriberDiffCollector(Subscriber subscriber) {
+ super(subscriber);
+ }
+
+ @Override
+ protected boolean hasMembers(IResource resource) {
+ return tree.members(resource).length > 0;
+ }
+
+ @Override
+ protected void remove(IResource resource) {
+ SubscriberDiffTreeEventHandler.this.remove(resource);
+ }
+
+ @Override
+ protected void change(IResource resource, int depth) {
+ SubscriberDiffTreeEventHandler.this.change(resource, depth);
+ }
+ }
+
+ /**
+ * Create the handler
+ * @param subscriber the subscriber for the handler
+ * @param manager the scope of the handler
+ * @param tree the tree to be populated by this handler
+ * @param filter a filter
+ */
+ public SubscriberDiffTreeEventHandler(Subscriber subscriber, ISynchronizationScopeManager manager, ResourceDiffTree tree, DiffFilter filter) {
+ super(subscriber, manager.getScope());
+ this.manager = manager;
+ this.tree = tree;
+ this.collector = new SubscriberDiffCollector(subscriber);
+ this.filter = filter;
+ }
+
+ @Override
+ protected void reset(ResourceTraversal[] traversals, int type) {
+ // Reset the exception state since we are reseting
+ exceptionState = EXCEPTION_NONE;
+ if (!manager.isInitialized() && state == STATE_OK_TO_INITIALIZE) {
+ // This means the scope has not been initialized
+ queueEvent(new RunnableEvent(monitor -> {
+ // Only initialize the scope if we are in the STARTED state
+ if (state == STATE_OK_TO_INITIALIZE) {
+ try {
+ prepareScope(monitor);
+ state = STATE_COLLECTING_CHANGES;
+ } finally {
+ // If the initialization didn't complete,
+ // return to the STARTED state.
+ if (state != STATE_COLLECTING_CHANGES) {
+ state = STATE_STARTED;
+ if (exceptionState == EXCEPTION_NONE)
+ exceptionState = EXCEPTION_CANCELED;
+ }
+ }
+ }
+ }, true), true);
+ } else if (manager.isInitialized()) {
+ state = STATE_COLLECTING_CHANGES;
+ super.reset(traversals, type);
+ }
+ }
+
+ public void reset(){
+ reset(getScope().getTraversals(),
+ SubscriberEventHandler.SubscriberEvent.INITIALIZE);
+ }
+
+ protected void prepareScope(IProgressMonitor monitor) {
+ try {
+ manager.initialize(monitor);
+ } catch (CoreException e) {
+ handleException(e);
+ }
+ ResourceTraversal[] traversals = manager.getScope().getTraversals();
+ if (traversals.length > 0)
+ reset(traversals, SubscriberEvent.INITIALIZE);
+ }
+
+ @Override
+ protected void handleChange(IResource resource) throws CoreException {
+ IDiff node = getSubscriber().getDiff(resource);
+ if (node == null) {
+ queueDispatchEvent(
+ new SubscriberEvent(resource, SubscriberEvent.REMOVAL, IResource.DEPTH_ZERO));
+ } else {
+ if (isInScope(resource))
+ queueDispatchEvent(
+ new SubscriberDiffChangedEvent(resource, SubscriberEvent.CHANGE, IResource.DEPTH_ZERO, node));
+ }
+ }
+
+ private boolean isInScope(IResource resource) {
+ return manager.getScope().contains(resource);
+ }
+
+ @Override
+ protected void collectAll(IResource resource, int depth,
+ final IProgressMonitor monitor) {
+ Policy.checkCanceled(monitor);
+ monitor.beginTask(null, IProgressMonitor.UNKNOWN);
+ ResourceTraversal[] traversals = new ResourceTraversal[] { new ResourceTraversal(new IResource[] { resource }, depth, IResource.NONE) };
+ try {
+ getSubscriber().accept(traversals, diff -> {
+ Policy.checkCanceled(monitor);
+ monitor.subTask(NLS.bind(Messages.SubscriberDiffTreeEventHandler_0, tree.getResource(diff).getFullPath().toString()));
+ // Queue up any found diffs for inclusion into the output tree
+ queueDispatchEvent(
+ new SubscriberDiffChangedEvent(tree.getResource(diff), SubscriberEvent.CHANGE, IResource.DEPTH_ZERO, diff));
+ // Handle any pending dispatches
+ handlePreemptiveEvents(monitor);
+ handlePendingDispatch(monitor);
+ return true;
+ });
+ } catch (CoreException e) {
+ if (resource.getProject().isAccessible())
+ handleException(e, resource, ITeamStatus.SYNC_INFO_SET_ERROR, e.getMessage());
+ } finally {
+ monitor.done();
+ }
+ }
+
+ @Override
+ protected void dispatchEvents(SubscriberEvent[] events,
+ IProgressMonitor monitor) {
+ try {
+ tree.beginInput();
+ for (int i = 0; i < events.length; i++) {
+ SubscriberEvent event = events[i];
+ switch (event.getType()) {
+ case SubscriberEvent.CHANGE :
+ if (event instanceof SubscriberDiffChangedEvent) {
+ SubscriberDiffChangedEvent se = (SubscriberDiffChangedEvent) event;
+ IDiff changedNode = se.getChangedNode();
+ if (changedNode.getKind() == IDiff.NO_CHANGE) {
+ tree.remove(changedNode.getPath());
+ } else {
+ addDiff(changedNode, monitor);
+ }
+ }
+ break;
+ case SubscriberEvent.REMOVAL :
+ IDiff[] nodesToRemove = tree.getDiffs(new ResourceTraversal[] { event.asTraversal() });
+ for (int j = 0; j < nodesToRemove.length; j++) {
+ IDiff node = nodesToRemove[j];
+ tree.remove(node.getPath());
+ }
+ break;
+ }
+ }
+ } finally {
+ tree.endInput(monitor);
+ }
+ }
+
+ private void addDiff(IDiff diff, IProgressMonitor monitor) {
+ if (filter == null || filter.select(diff, monitor)) {
+ tree.add(diff);
+ } else {
+ tree.remove(diff.getPath());
+ }
+ }
+
+ /**
+ * Return the resource diff tree that contains the out-of-sync diffs for the
+ * subscriber.
+ * @return the resource diff tree
+ */
+ public IResourceDiffTree getTree() {
+ return tree;
+ }
+
+ @Override
+ public Subscriber getSubscriber() {
+ return super.getSubscriber();
+ }
+
+ @Override
+ public void shutdown() {
+ state = STATE_SHUTDOWN;
+ collector.dispose();
+ super.shutdown();
+ }
+
+ @Override
+ protected Object getJobFamiliy() {
+ return family;
+ }
+
+ /**
+ * Set the family of this handler to the given object
+ * @param family the family of the handler's job
+ */
+ public void setJobFamily(Object family) {
+ this.family = family;
+ }
+
+ @Override
+ protected void handleException(CoreException e, IResource resource, int code, String message) {
+ super.handleException(e, resource, code, message);
+ tree.reportError(new TeamStatus(IStatus.ERROR, TeamPlugin.ID, code, message, e, resource));
+ exceptionState = EXCEPTION_ERROR;
+ }
+
+ @Override
+ protected void handleCancel(OperationCanceledException e) {
+ super.handleCancel(e);
+ tree.reportError(new TeamStatus(IStatus.ERROR, TeamPlugin.ID, ITeamStatus.SYNC_INFO_SET_CANCELLATION, Messages.SubscriberEventHandler_12, e, ResourcesPlugin.getWorkspace().getRoot()));
+ if (exceptionState == EXCEPTION_NONE)
+ exceptionState = EXCEPTION_CANCELED;
+ }
+
+ public DiffFilter getFilter() {
+ return filter;
+ }
+
+ public void setFilter(DiffFilter filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * If the handler is not initialized or not in the process
+ * of initializing, start the initialization process.
+ */
+ public synchronized void initializeIfNeeded() {
+ if (state == STATE_STARTED) {
+ state = STATE_OK_TO_INITIALIZE;
+ reset(getScope().getTraversals(), SubscriberEvent.INITIALIZE);
+ } else if (exceptionState != EXCEPTION_NONE) {
+ reset(getScope().getTraversals(), SubscriberEvent.INITIALIZE);
+ }
+ }
+
+ @Override
+ public synchronized void start() {
+ super.start();
+ if (state == STATE_NEW)
+ state = STATE_STARTED;
+ }
+
+ public int getState() {
+ return state;
+ }
+
+ @Override
+ protected boolean isSystemJob() {
+ if (manager != null && !manager.isInitialized())
+ return false;
+ return super.isSystemJob();
+ }
+
+ @Override
+ public synchronized void remove(IResource resource) {
+ // Don't queue changes if we haven't been initialized
+ if (state == STATE_STARTED)
+ return;
+ super.remove(resource);
+ }
+
+ @Override
+ public void change(IResource resource, int depth) {
+ // Don't queue changes if we haven't been initialized
+ if (state == STATE_STARTED)
+ return;
+ super.change(resource, depth);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberEventHandler.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberEventHandler.java
new file mode 100644
index 000000000..04ac32b72
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberEventHandler.java
@@ -0,0 +1,415 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRunnable;
+import org.eclipse.core.resources.mapping.ResourceTraversal;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.ITeamStatus;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.mapping.ISynchronizationScope;
+import org.eclipse.team.core.mapping.ISynchronizationScopeChangeListener;
+import org.eclipse.team.core.subscribers.Subscriber;
+import org.eclipse.team.internal.core.*;
+
+/**
+ * This handler collects changes and removals to resources and calculates their
+ * synchronization state in a background job. The result is fed input the SyncSetInput.
+ *
+ * Exceptions that occur when the job is processing the events are collected and
+ * returned as part of the Job's status.
+ */
+public abstract class SubscriberEventHandler extends BackgroundEventHandler {
+
+ // Changes accumulated by the event handler
+ private List<Event> resultCache = new ArrayList<>();
+
+ private boolean started = false;
+ private boolean initializing = true;
+
+ private IProgressMonitor progressGroup;
+
+ private int ticks;
+
+ private final Subscriber subscriber;
+ private ISynchronizationScope scope;
+
+ private ISynchronizationScopeChangeListener scopeChangeListener;
+
+ /**
+ * Internal resource synchronization event. Can contain a result.
+ */
+ class SubscriberEvent extends ResourceEvent{
+ static final int REMOVAL = 1;
+ static final int CHANGE = 2;
+ static final int INITIALIZE = 3;
+
+ SubscriberEvent(IResource resource, int type, int depth) {
+ super(resource, type, depth);
+ }
+ @Override
+ protected String getTypeString() {
+ switch (getType()) {
+ case REMOVAL :
+ return "REMOVAL"; //$NON-NLS-1$
+ case CHANGE :
+ return "CHANGE"; //$NON-NLS-1$
+ case INITIALIZE :
+ return "INITIALIZE"; //$NON-NLS-1$
+ default :
+ return "INVALID"; //$NON-NLS-1$
+ }
+ }
+ public ResourceTraversal asTraversal() {
+ return new ResourceTraversal(new IResource[] { getResource() }, getDepth(), IResource.NONE);
+ }
+ }
+
+ /**
+ * Create a handler. This will initialize all resources for the subscriber associated with
+ * the set.
+ * @param subscriber the subscriber
+ * @param scope the scope
+ */
+ public SubscriberEventHandler(Subscriber subscriber, ISynchronizationScope scope) {
+ super(
+ NLS.bind(Messages.SubscriberEventHandler_jobName, new String[] { subscriber.getName() }),
+ NLS.bind(Messages.SubscriberEventHandler_errors, new String[] { subscriber.getName() }));
+ this.subscriber = subscriber;
+ this.scope = scope;
+ scopeChangeListener = (scope1, newMappings, newTraversals) -> reset(new ResourceTraversal[0], scope1.getTraversals());
+ this.scope.addScopeChangeListener(scopeChangeListener);
+ }
+
+ /**
+ * The traversals of the scope have changed
+ * @param oldTraversals the old traversals
+ * @param newTraversals the new traversals
+ */
+ protected synchronized void reset(ResourceTraversal[] oldTraversals, ResourceTraversal[] newTraversals) {
+ reset(newTraversals, SubscriberEvent.CHANGE);
+ }
+
+ /**
+ * Start the event handler by queuing events to prime the sync set input with the out-of-sync
+ * resources of the subscriber.
+ */
+ public synchronized void start() {
+ // Set the started flag to enable event queuing.
+ // We are guaranteed to be the first since this method is synchronized.
+ started = true;
+ ResourceTraversal[] traversals = scope.getTraversals();
+ reset(traversals, SubscriberEvent.INITIALIZE);
+ initializing = false;
+ }
+
+ @Override
+ protected synchronized void queueEvent(Event event, boolean front) {
+ // Only post events if the handler is started
+ if (started) {
+ super.queueEvent(event, front);
+ }
+ }
+ /**
+ * Schedule the job or process the events now.
+ */
+ @Override
+ public void schedule() {
+ Job job = getEventHandlerJob();
+ if (job.getState() == Job.NONE) {
+ if(progressGroup != null) {
+ job.setSystem(false);
+ job.setProgressGroup(progressGroup, ticks);
+ } else {
+ job.setSystem(isSystemJob());
+ }
+ }
+ getEventHandlerJob().schedule();
+ }
+
+ protected boolean isSystemJob() {
+ return !initializing;
+ }
+
+
+ @Override
+ protected void jobDone(IJobChangeEvent event) {
+ super.jobDone(event);
+ progressGroup = null;
+ }
+
+ /**
+ * Called by a client to indicate that a resource has changed and its synchronization state
+ * should be recalculated.
+ * @param resource the changed resource
+ * @param depth the depth of the change calculation
+ */
+ public void change(IResource resource, int depth) {
+ queueEvent(new SubscriberEvent(resource, SubscriberEvent.CHANGE, depth), false);
+ }
+
+ /**
+ * Called by a client to indicate that a resource has been removed and should be removed. The
+ * removal will propagate to the set.
+ * @param resource the resource that was removed
+ */
+ public void remove(IResource resource) {
+ queueEvent(
+ new SubscriberEvent(resource, SubscriberEvent.REMOVAL, IResource.DEPTH_INFINITE), false);
+ }
+
+ /**
+ * Collect the calculated synchronization information for the given resource at the given depth. The
+ * results are added to the provided list.
+ */
+ private void collect(
+ IResource resource,
+ int depth,
+ IProgressMonitor monitor) {
+
+ Policy.checkCanceled(monitor);
+
+ // handle any preemptive events before continuing
+ handlePreemptiveEvents(monitor);
+
+ if (resource.getType() != IResource.FILE
+ && depth != IResource.DEPTH_ZERO) {
+ try {
+ IResource[] members =
+ getSubscriber().members(resource);
+ for (int i = 0; i < members.length; i++) {
+ collect(
+ members[i],
+ depth == IResource.DEPTH_INFINITE
+ ? IResource.DEPTH_INFINITE
+ : IResource.DEPTH_ZERO,
+ monitor);
+ }
+ } catch (TeamException e) {
+ // We only handle the exception if the resource's project is accessible.
+ // The project close delta will clean up.
+ if (resource.getProject().isAccessible())
+ handleException(e, resource, ITeamStatus.SYNC_INFO_SET_ERROR, NLS.bind(Messages.SubscriberEventHandler_8, new String[] { resource.getFullPath().toString(), e.getMessage() }));
+ }
+ }
+
+ monitor.subTask(NLS.bind(Messages.SubscriberEventHandler_2, new String[] { resource.getFullPath().toString() }));
+ try {
+ handleChange(resource);
+ handlePendingDispatch(monitor);
+ } catch (CoreException e) {
+ handleException(e, resource, ITeamStatus.RESOURCE_SYNC_INFO_ERROR, NLS.bind(Messages.SubscriberEventHandler_9, new String[] { resource.getFullPath().toString(), e.getMessage() }));
+ }
+ monitor.worked(1);
+ }
+
+ /**
+ * Return the subscriber associated with this event handler
+ * @return the subscriber associated with this event handler
+ */
+ protected Subscriber getSubscriber() {
+ return subscriber;
+ }
+
+ /**
+ * The given resource has changed. Subclasses should handle
+ * this in an appropriate fashion
+ * @param resource the resource whose state has changed
+ */
+ protected abstract void handleChange(IResource resource) throws CoreException;
+
+ protected void handlePendingDispatch(IProgressMonitor monitor) {
+ if (isReadyForDispatch(false /*don't wait if queue is empty*/)) {
+ try {
+ dispatchEvents(Policy.subMonitorFor(monitor, 5));
+ } catch (TeamException e) {
+ handleException(e, null, ITeamStatus.SYNC_INFO_SET_ERROR, e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Handle the exception by returning it as a status from the job but also by
+ * dispatching it to the sync set input so any down stream views can react
+ * accordingly.
+ * The resource passed may be null.
+ */
+ protected void handleException(CoreException e, IResource resource, int code, String message) {
+ handleException(e);
+ }
+
+ /**
+ * Called to initialize to calculate the synchronization information using the optimized subscriber method. For
+ * subscribers that don't support the optimization, all resources in the subscriber are manually re-calculated.
+ * @param resource the resources to check
+ * @param depth the depth
+ * @param monitor
+ */
+ protected abstract void collectAll(
+ IResource resource,
+ int depth,
+ IProgressMonitor monitor);
+
+ /**
+ * Feed the given events to the set. The appropriate method on the set is called
+ * for each event type.
+ * @param events
+ */
+ protected abstract void dispatchEvents(SubscriberEvent[] events, IProgressMonitor monitor);
+
+ /**
+ * Initialize all resources for the subscriber associated with the set. This will basically recalculate
+ * all synchronization information for the subscriber.
+ * @param type can be Event.CHANGE to recalculate all states or Event.INITIALIZE to perform the
+ * optimized recalculation if supported by the subscriber.
+ */
+ protected void reset(ResourceTraversal[] traversals, int type) {
+ for (int i = 0; i < traversals.length; i++) {
+ ResourceTraversal traversal = traversals[i];
+ IResource[] resources = traversal.getResources();
+ for (int j = 0; j < resources.length; j++) {
+ queueEvent(new SubscriberEvent(resources[j], type, traversal.getDepth()), false);
+ }
+ }
+ }
+
+ @Override
+ protected void processEvent(Event event, IProgressMonitor monitor) {
+ try {
+ // Cancellation is dangerous because this will leave the sync info in a bad state.
+ // Purposely not checking -
+ int type = event.getType();
+ switch (type) {
+ case BackgroundEventHandler.RUNNABLE_EVENT :
+ executeRunnable(event, monitor);
+ break;
+ case SubscriberEvent.REMOVAL :
+ queueDispatchEvent(event);
+ break;
+ case SubscriberEvent.CHANGE :
+ collect(
+ event.getResource(),
+ ((ResourceEvent)event).getDepth(),
+ monitor);
+ break;
+ case SubscriberEvent.INITIALIZE :
+ monitor.subTask(NLS.bind(Messages.SubscriberEventHandler_2, new String[] { event.getResource().getFullPath().toString() }));
+ collectAll(
+ event.getResource(),
+ ((ResourceEvent)event).getDepth(),
+ Policy.subMonitorFor(monitor, 64));
+ break;
+ }
+ } catch (OperationCanceledException e) {
+ // the job has been canceled.
+ // Clear the queue and propagate the cancellation through the sets.
+ handleCancel(e);
+ } catch (RuntimeException e) {
+ // handle the exception and keep processing
+ if (event.getType() == BackgroundEventHandler.RUNNABLE_EVENT ) {
+ handleException(new TeamException(Messages.SubscriberEventHandler_10, e));
+ } else {
+ handleException(new TeamException(Messages.SubscriberEventHandler_10, e), event.getResource(), ITeamStatus.SYNC_INFO_SET_ERROR, NLS.bind(Messages.SubscriberEventHandler_11, new String[] { event.getResource().getFullPath().toString(), e.getMessage() }));
+ }
+ }
+ }
+
+ /**
+ * Queue the event to be handle during the dispatch phase.
+ * @param event the event
+ */
+ protected void queueDispatchEvent(Event event) {
+ resultCache.add(event);
+ }
+
+ /**
+ * Handle the cancel exception
+ * @param e the cancel exception
+ */
+ protected void handleCancel(OperationCanceledException e) {
+ resultCache.clear();
+ }
+
+ /*
+ * 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, null, ITeamStatus.SYNC_INFO_SET_ERROR, e.getMessage());
+ }
+ try {
+ ((RunnableEvent)event).run(Policy.subMonitorFor(monitor, 1));
+ } catch (CoreException e) {
+ handleException(e, null, ITeamStatus.SYNC_INFO_SET_ERROR, e.getMessage());
+ }
+ }
+
+ @Override
+ protected boolean doDispatchEvents(IProgressMonitor monitor) {
+ if (!resultCache.isEmpty()) {
+ dispatchEvents(resultCache.toArray(new SubscriberEvent[resultCache.size()]), monitor);
+ resultCache.clear();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Queue up the given runnable in an event to be processed by this job
+ *
+ * @param runnable
+ * the runnable to be run by the handler
+ * @param frontOnQueue
+ * the frontOnQueue flag is used to indicate that the runnable
+ * should be placed on the front of the queue and be processed as
+ * soon as possible
+ */
+ public void run(IWorkspaceRunnable runnable, boolean frontOnQueue) {
+ queueEvent(new RunnableEvent(runnable, frontOnQueue), frontOnQueue);
+ }
+
+ public void setProgressGroupHint(IProgressMonitor progressGroup, int ticks) {
+ this.progressGroup = progressGroup;
+ this.ticks = ticks;
+ }
+
+ protected void handlePreemptiveEvents(IProgressMonitor monitor) {
+ Event event = peek();
+ if (event instanceof RunnableEvent && ((RunnableEvent)event).isPreemtive()) {
+ executeRunnable(nextElement(), monitor);
+ }
+ }
+
+ /**
+ * Return the scope of this event handler. The scope is
+ * used to determine the resources that are processed by the handler
+ * @return the scope of this event handler
+ */
+ protected ISynchronizationScope getScope() {
+ return scope;
+ }
+
+ @Override
+ public void shutdown() {
+ super.shutdown();
+ scope.removeScopeChangeListener(scopeChangeListener);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberResourceCollector.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberResourceCollector.java
new file mode 100644
index 000000000..de97e030f
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberResourceCollector.java
@@ -0,0 +1,233 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.team.core.subscribers.*;
+
+/**
+ * This class acts as a superclass for any class that is collecting subscriber
+ * resources. It provides functionality that listens to resource deltas and
+ * subscriber change events in order to determine when the state of resources
+ * that are supervised by a subscriber may have changed.
+ */
+public abstract class SubscriberResourceCollector implements IResourceChangeListener, ISubscriberChangeListener {
+
+ Subscriber subscriber;
+
+ /**
+ * Create the collector and register it as a listener with the workspace
+ * and the subscriber.
+ * @param subscriber the subscriber to be associated with this collector
+ */
+ public SubscriberResourceCollector(Subscriber subscriber) {
+ Assert.isNotNull(subscriber);
+ this.subscriber = subscriber;
+ ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
+ subscriber.addListener(this);
+ }
+
+ /**
+ * Returns the <code>Subscriber</code> associated with this collector.
+ *
+ * @return the <code>Subscriber</code> associated with this collector.
+ */
+ public Subscriber getSubscriber() {
+ return subscriber;
+ }
+
+ /**
+ * De-register the listeners for this collector.
+ */
+ public void dispose() {
+ getSubscriber().removeListener(this);
+ ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
+ }
+
+ @Override
+ public void subscriberResourceChanged(ISubscriberChangeEvent[] deltas) {
+ try {
+ beginInput();
+ IResource[] roots = getRoots();
+ for (int i = 0; i < deltas.length; i++) {
+ switch (deltas[i].getFlags()) {
+ case ISubscriberChangeEvent.SYNC_CHANGED :
+ if (isAllRootsIncluded() || isDescendantOfRoot(deltas[i].getResource(), roots)) {
+ change(deltas[i].getResource(), IResource.DEPTH_ZERO);
+ }
+ break;
+ case ISubscriberChangeEvent.ROOT_REMOVED :
+ remove(deltas[i].getResource());
+ break;
+ case ISubscriberChangeEvent.ROOT_ADDED :
+ if (isAllRootsIncluded() || isDescendantOfRoot(deltas[i].getResource(), roots)) {
+ change(deltas[i].getResource(), IResource.DEPTH_INFINITE);
+ }
+ break;
+ }
+ }
+ } finally {
+ endInput();
+ }
+ }
+
+ /**
+ * This method is invoked at the beginning of a subscriber change event
+ * or resource delta event. The endInput method will be invoked at some point
+ * following this. There may be several invocations of remove or change
+ * in between.
+ */
+ protected void beginInput() {
+ // Do nothing by default
+ }
+
+ /**
+ * The processing of the resource or subscriber delta has finished.
+ * Subclasses can accumulate removals and changes and handle them
+ * at this point to allow batched change events.
+ */
+ protected void endInput() {
+ // Do nothing by default
+ }
+
+
+ @Override
+ public void resourceChanged(IResourceChangeEvent event) {
+ try {
+ beginInput();
+ processDelta(event.getDelta(), getRoots());
+ } finally {
+ endInput();
+ }
+ }
+
+ /**
+ * Process the resource delta and posts all necessary events to the background
+ * event handler.
+ *
+ * @param delta the resource delta to analyze
+ */
+ protected void processDelta(IResourceDelta delta, IResource[] roots) {
+ IResource resource = delta.getResource();
+ int kind = delta.getKind();
+
+ if (resource.getType() == IResource.PROJECT) {
+ // Handle projects that should be removed from the collector
+ if (((kind & IResourceDelta.REMOVED) != 0) /* deleted project */
+ || (delta.getFlags() & IResourceDelta.OPEN) != 0 && !((IProject) resource).isOpen() /* closed project */
+ || !isAncestorOfRoot(resource, roots)) /* not within subscriber roots */ {
+ // If the project has any entries in the sync set, remove them
+ if (hasMembers(resource)) {
+ remove(resource);
+ }
+ }
+ }
+
+ boolean visitChildren = false;
+ if (isDescendantOfRoot(resource, roots)) {
+ visitChildren = true;
+ // If the resource has changed type, remove the old resource handle
+ // and add the new one
+ if ((delta.getFlags() & IResourceDelta.TYPE) != 0) {
+ remove(resource);
+ change(resource, IResource.DEPTH_INFINITE);
+ }
+
+ // Check the flags for changes the SyncSet cares about.
+ // Notice we don't care about MARKERS currently.
+ int changeFlags = delta.getFlags();
+ if ((changeFlags & (IResourceDelta.OPEN | IResourceDelta.CONTENT)) != 0) {
+ change(resource, IResource.DEPTH_ZERO);
+ }
+
+ // Check the kind and deal with those we care about
+ if ((delta.getKind() & (IResourceDelta.REMOVED | IResourceDelta.ADDED)) != 0) {
+ change(resource, IResource.DEPTH_ZERO);
+ }
+ }
+
+ // Handle changed children
+ if (visitChildren || isAncestorOfRoot(resource, roots)) {
+ IResourceDelta[] affectedChildren = delta.getAffectedChildren(IResourceDelta.CHANGED | IResourceDelta.REMOVED | IResourceDelta.ADDED);
+ for (int i = 0; i < affectedChildren.length; i++) {
+ processDelta(affectedChildren[i], roots);
+ }
+ }
+ }
+
+ /**
+ * Return the root resources that are to be considered by this handler.
+ * These may be either the subscriber roots or a set of resources that are
+ * contained by the subscriber's roots.
+ * @return the root resources that are to be considered by this handler
+ */
+ protected IResource[] getRoots() {
+ return getSubscriber().roots();
+ }
+
+ /**
+ * Return whether the given resource, which is not
+ * within the roots of this handler, has children
+ * that are.
+ * @param resource the resource
+ * @return whether the resource has children that are being considered
+ * by this handler.
+ */
+ protected abstract boolean hasMembers(IResource resource);
+
+ /**
+ * The resource is no longer of concern to the subscriber.
+ * Remove the resource and any of it's descendants
+ * from the set of resources being collected.
+ * @param resource the resource to be removed along with its
+ * descendants.
+ */
+ protected abstract void remove(IResource resource);
+
+ /**
+ * The resource sync state has changed to the depth specified.
+ * @param resource the resource
+ * @param depth the depth
+ */
+ protected abstract void change(IResource resource, int depth);
+
+ /**
+ * Return whether all roots of a subscriber are included or
+ * if the collector is only consider a subset of the resources.
+ * @return whether all roots of a subscriber are included
+ */
+ protected boolean isAllRootsIncluded() {
+ return true;
+ }
+
+ private boolean isAncestorOfRoot(IResource parent, IResource[] roots) {
+ // Always traverse into projects in case a root was removed
+ if (parent.getType() == IResource.ROOT) return true;
+ for (int i = 0; i < roots.length; i++) {
+ IResource resource = roots[i];
+ if (parent.getFullPath().isPrefixOf(resource.getFullPath())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isDescendantOfRoot(IResource resource, IResource[] roots) {
+ for (int i = 0; i < roots.length; i++) {
+ IResource root = roots[i];
+ if (root.getFullPath().isPrefixOf(resource.getFullPath())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoCollector.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoCollector.java
new file mode 100644
index 000000000..ba554f3cc
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoCollector.java
@@ -0,0 +1,207 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.team.core.subscribers.Subscriber;
+import org.eclipse.team.core.synchronize.*;
+import org.eclipse.team.internal.core.Policy;
+
+/**
+ * This collector maintains a {@link SyncInfoSet} for a particular team subscriber keeping
+ * it up-to-date with both incoming changes and outgoing changes as they occur for
+ * resources in the workspace. The collector can be configured to consider all the subscriber's
+ * roots or only a subset.
+ * <p>
+ * The advantage of this collector is that it processes both resource and team
+ * subscriber deltas in a background thread.
+ * </p>
+ * @since 3.0
+ */
+public final class SubscriberSyncInfoCollector extends SubscriberResourceCollector {
+
+ private final SyncSetInputFromSubscriber subscriberInput;
+ private SyncSetInputFromSyncSet filteredInput;
+ private SubscriberSyncInfoEventHandler eventHandler;
+ private IResource[] roots;
+
+ /**
+ * Create a collector that collects out-of-sync resources that are children of
+ * the given roots. If the roots are <code>null</code>, then all out-of-sync resources
+ * from the subscriber are collected. An empty array of roots will cause no resources
+ * to be collected. The <code>start()</code> method must be called after creation
+ * to prime the collector's sync sets.
+ * @param subscriber the Subscriber
+ * @param roots the roots of the out-of-sync resources to be collected
+ */
+ public SubscriberSyncInfoCollector(Subscriber subscriber, IResource[] roots) {
+ super(subscriber);
+ this.roots = roots;
+ this.eventHandler = new SubscriberSyncInfoEventHandler(subscriber, roots);
+ this.subscriberInput = eventHandler.getSyncSetInput();
+ filteredInput = new SyncSetInputFromSyncSet(subscriberInput.getSyncSet(), getEventHandler());
+ filteredInput.setFilter(new SyncInfoFilter() {
+ @Override
+ public boolean select(SyncInfo info, IProgressMonitor monitor) {
+ return true;
+ }
+ });
+
+ }
+
+ public void setProgressGroup(IProgressMonitor monitor, int ticks) {
+ getEventHandler().setProgressGroupHint(monitor, ticks);
+ }
+
+ /**
+ * Start the collector.
+ */
+ public void start() {
+ eventHandler.start();
+ }
+
+ /**
+ * This causes the calling thread to wait any background collection of
+ * out-of-sync resources to stop before returning.
+ *
+ * @param monitor
+ * a progress monitor
+ */
+ public void waitForCollector(IProgressMonitor monitor) {
+ monitor.worked(1);
+ int i = 0;
+ // wait for the event handler to process changes
+ while (true) {
+ try {
+ Thread.sleep(5);
+ } catch (InterruptedException e) {
+ }
+ Policy.checkCanceled(monitor);
+
+ // increment the counter or reset it if the job is running
+ i = (eventHandler.getEventHandlerJob().getState() == Job.NONE) ? i + 1 : 0;
+
+ // 50 positive checks in a row
+ if (i == 50)
+ break;
+ }
+ monitor.worked(1);
+ }
+
+ /**
+ * Clears this collector's sync info sets and causes them to be recreated from the
+ * associated <code>Subscriber</code>. The reset will occur in the background. If the
+ * caller wishes to wait for the reset to complete, they should call
+ * waitForCollector(IProgressMonitor).
+ */
+ public void reset() {
+ eventHandler.reset(getRoots());
+ }
+
+ /**
+ * Disposes of the background job associated with this collector and de-registers
+ * all it's listeners. This method must be called when the collector is no longer
+ * referenced and could be garbage collected.
+ */
+ @Override
+ public void dispose() {
+ eventHandler.shutdown();
+ subscriberInput.disconnect();
+ if(filteredInput != null) {
+ filteredInput.disconnect();
+ }
+ super.dispose();
+ }
+
+ /**
+ * Return the roots that are being considered by this collector.
+ * By default, the collector is interested in the roots of its
+ * subscriber. However, the set can be reduced using {@link #setRoots(IResource[])}.
+ * @return the roots
+ */
+ @Override
+ public IResource[] getRoots() {
+ if (roots == null) {
+ return super.getRoots();
+ } else {
+ return roots;
+ }
+ }
+
+ /*
+ * Returns whether the collector is configured to collect for
+ * all roots of the subscriber or not
+ * @return <code>true</code> if the collector is considering all
+ * roots of the subscriber and <code>false</code> otherwise
+ */
+ @Override
+ public boolean isAllRootsIncluded() {
+ return roots == null;
+ }
+
+ /**
+ * Return the event handler that performs the background processing for this collector.
+ * The event handler also serves the purpose of serializing the modifications and adjustments
+ * to the collector's sync sets in order to ensure that the state of the sets is kept
+ * consistent.
+ * @return Returns the eventHandler.
+ */
+ protected SubscriberEventHandler getEventHandler() {
+ return eventHandler;
+ }
+
+ /**
+ * Return the <code>SyncInfoSet</code> that contains all the out-of-sync resources for the
+ * subscriber that are descendants of the roots of this collector. The set will contain only those resources that are children of the roots
+ * of the collector unless the roots of the collector has been set to <code>null</code>
+ * in which case all out-of-sync resources from the subscriber are collected.
+ * @return the subscriber sync info set
+ */
+ public SyncInfoTree getSubscriberSyncInfoSet() {
+ return subscriberInput.getSyncSet();
+ }
+
+ public SyncInfoTree getSyncInfoSet() {
+ return filteredInput.getSyncSet();
+ }
+
+ /**
+ * Set the filter for this collector. Only elements that match the filter will
+ * be in the out sync info set.
+ * @param filter the sync info filter
+ */
+ public void setFilter(SyncInfoFilter filter) {
+ filteredInput.setFilter(filter);
+ filteredInput.reset();
+ }
+
+ public void setRoots(IResource[] roots) {
+ this.roots = roots;
+ reset();
+ }
+
+ @Override
+ protected boolean hasMembers(IResource resource) {
+ return getSubscriberSyncInfoSet().hasMembers(resource);
+ }
+
+ @Override
+ protected void remove(IResource resource) {
+ eventHandler.remove(resource);
+ }
+
+ @Override
+ protected void change(IResource resource, int depth) {
+ eventHandler.change(resource, depth);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoEventHandler.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoEventHandler.java
new file mode 100644
index 000000000..2a4a0c63a
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoEventHandler.java
@@ -0,0 +1,217 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.resources.mapping.ResourceTraversal;
+import org.eclipse.core.runtime.*;
+import org.eclipse.team.core.*;
+import org.eclipse.team.core.mapping.ISynchronizationScope;
+import org.eclipse.team.core.subscribers.Subscriber;
+import org.eclipse.team.core.synchronize.*;
+import org.eclipse.team.internal.core.Messages;
+import org.eclipse.team.internal.core.TeamPlugin;
+
+/**
+ * An event handler that collects {@link SyncInfo} in a {@link SyncInfoTree}.
+ */
+public class SubscriberSyncInfoEventHandler extends SubscriberEventHandler {
+
+ // The set that receives notification when the resource synchronization state
+ // has been calculated by the job.
+ private final SyncSetInputFromSubscriber syncSetInput;
+
+ private class SubscriberSyncInfoEvent extends SubscriberEvent {
+ private final SyncInfo result;
+
+ public SubscriberSyncInfoEvent(IResource resource, int type, int depth, SyncInfo result) {
+ super(resource, type, depth);
+ this.result = result;
+ }
+ public SyncInfo getResult() {
+ return result;
+ }
+ }
+
+ public static ISynchronizationScope createScope(IResource[] roots, Subscriber subscriber) {
+ if (roots == null)
+ roots = subscriber.roots();
+ return new RootResourceSynchronizationScope(roots);
+ }
+
+ /**
+ * Create the event handler for the given subscriber and roots
+ * @param subscriber the subscriber
+ * @param roots the roots or <code>null</code> if the roots from the subscriber
+ * should be used.
+ */
+ public SubscriberSyncInfoEventHandler(final Subscriber subscriber, IResource[] roots) {
+ super(subscriber, createScope(roots, subscriber));
+ this.syncSetInput = new SyncSetInputFromSubscriber(subscriber, this);
+ }
+
+ @Override
+ protected void handleException(CoreException e, IResource resource, int code, String message) {
+ super.handleException(e, resource, code, message);
+ syncSetInput.handleError(new TeamStatus(IStatus.ERROR, TeamPlugin.ID, code, message, e, resource));
+ }
+
+ @Override
+ protected void handleCancel(OperationCanceledException e) {
+ super.handleCancel(e);
+ syncSetInput.handleError(new TeamStatus(IStatus.ERROR, TeamPlugin.ID, ITeamStatus.SYNC_INFO_SET_CANCELLATION, Messages.SubscriberEventHandler_12, e, ResourcesPlugin.getWorkspace().getRoot()));
+ }
+
+ /**
+ * Return the sync set input that was created by this event handler
+ * @return the sync set input
+ */
+ public SyncSetInputFromSubscriber getSyncSetInput() {
+ return syncSetInput;
+ }
+
+ @Override
+ protected void handleChange(IResource resource) throws TeamException {
+ SyncInfo info = syncSetInput.getSubscriber().getSyncInfo(resource);
+ // resource is no longer under the subscriber control
+ if (info == null) {
+ queueDispatchEvent(
+ new SubscriberEvent(resource, SubscriberEvent.REMOVAL, IResource.DEPTH_ZERO));
+ } else {
+ queueDispatchEvent(
+ new SubscriberSyncInfoEvent(resource, SubscriberEvent.CHANGE, IResource.DEPTH_ZERO, info));
+ }
+ }
+
+ @Override
+ protected void collectAll(
+ IResource resource,
+ int depth,
+ IProgressMonitor monitor) {
+
+ monitor.beginTask(null, IProgressMonitor.UNKNOWN);
+ try {
+
+ // Create a monitor that will handle preemption and dispatch if required
+ IProgressMonitor collectionMonitor = new SubProgressMonitor(monitor, IProgressMonitor.UNKNOWN) {
+ boolean dispatching = false;
+ @Override
+ public void subTask(String name) {
+ dispatch();
+ super.subTask(name);
+ }
+ private void dispatch() {
+ if (dispatching) return;
+ try {
+ dispatching = true;
+ handlePreemptiveEvents(this);
+ handlePendingDispatch(this);
+ } finally {
+ dispatching = false;
+ }
+ }
+ @Override
+ public void worked(int work) {
+ dispatch();
+ super.worked(work);
+ }
+ };
+
+ // Create a sync set that queues up resources and errors for dispatch
+ SyncInfoSet collectionSet = new SyncInfoSet() {
+ @Override
+ public void add(SyncInfo info) {
+ super.add(info);
+ queueDispatchEvent(
+ new SubscriberSyncInfoEvent(info.getLocal(), SubscriberEvent.CHANGE, IResource.DEPTH_ZERO, info));
+ }
+ @Override
+ public void addError(ITeamStatus status) {
+ if (status instanceof TeamStatus) {
+ TeamStatus ts = (TeamStatus) status;
+ IResource resource = ts.getResource();
+ if (resource != null && !resource.getProject().isAccessible()) {
+ // The project was closed while we were collecting sync info.
+ // The close delta will cause us to clean up properly
+ return;
+ }
+ }
+ super.addError(status);
+ TeamPlugin.getPlugin().getLog().log(status);
+ syncSetInput.handleError(status);
+ }
+ @Override
+ public void remove(IResource resource) {
+ super.remove(resource);
+ queueDispatchEvent(
+ new SubscriberEvent(resource, SubscriberEvent.REMOVAL, IResource.DEPTH_ZERO));
+ }
+ };
+
+ syncSetInput.getSubscriber().collectOutOfSync(new IResource[] { resource }, depth, collectionSet, collectionMonitor);
+
+ } finally {
+ monitor.done();
+ }
+ }
+
+ @Override
+ protected void dispatchEvents(SubscriberEvent[] events, IProgressMonitor monitor) {
+ // this will batch the following set changes until endInput is called.
+ SubscriberSyncInfoSet syncSet = syncSetInput.getSyncSet();
+ try {
+ syncSet.beginInput();
+ for (int i = 0; i < events.length; i++) {
+ SubscriberEvent event = events[i];
+ switch (event.getType()) {
+ case SubscriberEvent.CHANGE :
+ if (event instanceof SubscriberSyncInfoEvent) {
+ SubscriberSyncInfoEvent se = (SubscriberSyncInfoEvent) event;
+ syncSetInput.collect(se.getResult(), monitor);
+ }
+ break;
+ case SubscriberEvent.REMOVAL :
+ syncSet.remove(event.getResource(), event.getDepth());
+ break;
+ }
+ }
+ } finally {
+ syncSet.endInput(monitor);
+ }
+ }
+
+ /**
+ * Initialize all resources for the subscriber associated with the set. This
+ * will basically recalculate all synchronization information for the
+ * subscriber.
+ * <p>
+ * This method is synchronized with the queueEvent method to ensure that the
+ * two events queued by this method are back-to-back.
+ *
+ * @param roots
+ * the new roots or <code>null</code> if the roots from the
+ * subscriber should be used.
+ */
+ public void reset(IResource[] roots) {
+ RootResourceSynchronizationScope scope = (RootResourceSynchronizationScope)getScope();
+ if (roots == null)
+ roots = getSubscriber().roots();
+ scope.setRoots(roots);
+ }
+
+ @Override
+ protected synchronized void reset(ResourceTraversal[] oldTraversals, ResourceTraversal[] newTraversals) {
+ // First, reset the sync set input to clear the sync set
+ run(monitor -> syncSetInput.reset(monitor), false /* keep ordering the same */);
+ // Then, prime the set from the subscriber
+ super.reset(oldTraversals, newTraversals);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoSet.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoSet.java
new file mode 100644
index 000000000..be0aec992
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SubscriberSyncInfoSet.java
@@ -0,0 +1,74 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.synchronize.ISyncInfoSetChangeListener;
+import org.eclipse.team.core.synchronize.SyncInfoTree;
+import org.eclipse.team.internal.core.Policy;
+
+/**
+ * The <code>SubscriberSyncInfoSet</code> is a <code>SyncInfoSet</code> that provides the ability to add,
+ * remove and change <code>SyncInfo</code> and fires change event notifications to registered listeners.
+ * It also provides the ability
+ * to batch changes in a single change notification as well as optimizations for sync info retrieval.
+ *
+ * This class uses synchronized methods and synchronized blocks to protect internal data structures during both access
+ * and modify operations and uses an <code>ILock</code> to make modification operations thread-safe. The events
+ * are fired while this lock is held so clients responding to these events should not obtain their own internal locks
+ * while processing change events.
+ *
+ * TODO: Override modification methods to enforce use with handler
+ *
+ */
+public class SubscriberSyncInfoSet extends SyncInfoTree {
+
+ protected SubscriberEventHandler handler;
+
+ public SubscriberSyncInfoSet(SubscriberEventHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void connect(ISyncInfoSetChangeListener listener, IProgressMonitor monitor) {
+ if (handler == null) {
+ super.connect(listener, monitor);
+ } else {
+ connect(listener);
+ }
+ }
+
+ /**
+ * Variation of connect that does not need progress and does not throw an exception.
+ * Progress is provided by the background event handler and errors are passed through
+ * the chain to the view.
+ * @param listener
+ */
+ public void connect(final ISyncInfoSetChangeListener listener) {
+ if (handler == null) {
+ // Should only use this connect if the set has a handler
+ throw new UnsupportedOperationException();
+ } else {
+ handler.run(monitor -> {
+ try {
+ beginInput();
+ monitor.beginTask(null, 100);
+ removeSyncSetChangedListener(listener);
+ addSyncSetChangedListener(listener);
+ listener.syncInfoSetReset(SubscriberSyncInfoSet.this, Policy.subMonitorFor(monitor, 95));
+ } finally {
+ endInput(Policy.subMonitorFor(monitor, 5));
+ monitor.done();
+ }
+ }, true /* high priority */);
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncByteConverter.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncByteConverter.java
new file mode 100644
index 000000000..59d63bc24
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncByteConverter.java
@@ -0,0 +1,130 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2006 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.internal.core.subscribers;
+
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.internal.core.Messages;
+
+/**
+ * Utility for managing slash separated sync information fields.
+ */
+public class SyncByteConverter {
+
+ protected static final byte SEPARATOR_BYTE = (byte)'/';
+
+ /**
+ * Change the slot to the given bytes
+ * @param syncBytes the sync bytes
+ * @param slot the slot location
+ * @param newBytes the bytes to be put in the slot
+ * @return the new sync bytes
+ * @throws TeamException
+ */
+ public static byte[] setSlot(byte[] syncBytes, int slot, byte[] newBytes) throws TeamException {
+ int start = startOfSlot(syncBytes, slot);
+ if (start == -1) {
+ throw new TeamException(NLS.bind(Messages.SyncByteConverter_1, new String[] { new String(syncBytes) }));
+ }
+ int end = startOfSlot(syncBytes, slot + 1);
+ int totalLength = start + 1 + newBytes.length;
+ if (end != -1) {
+ totalLength += syncBytes.length - end;
+ }
+ byte[] result = new byte[totalLength];
+ System.arraycopy(syncBytes, 0, result, 0, start + 1);
+ System.arraycopy(newBytes, 0, result, start + 1, newBytes.length);
+ if (end != -1) {
+ System.arraycopy(syncBytes, end, result, start + 1 + newBytes.length, syncBytes.length - end);
+ }
+ return result;
+ }
+
+ /**
+ * Method startOfSlot returns the index of the slash that occurs before the
+ * given slot index. The provided index should be >= 1 which assumes that
+ * slot zero occurs before the first slash.
+ *
+ * @param syncBytes
+ * @param i
+ * @return int
+ */
+ private static int startOfSlot(byte[] syncBytes, int slot) {
+ int count = 0;
+ for (int j = 0; j < syncBytes.length; j++) {
+ if (syncBytes[j] == SEPARATOR_BYTE) {
+ count++;
+ if (count == slot) return j;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Return the offset the the Nth delimeter from the given start index.
+ * @param bytes
+ * @param delimiter
+ * @param start
+ * @param n
+ * @return int
+ */
+ private static int getOffsetOfDelimeter(byte[] bytes, byte delimiter, int start, int n) {
+ int count = 0;
+ for (int i = start; i < bytes.length; i++) {
+ if (bytes[i] == delimiter) count++;
+ if (count == n) return i;
+ }
+ // the Nth delimeter was not found
+ return -1;
+ }
+
+ /**
+ * Get the bytes in the given slot.
+ * @param bytes the sync bytes
+ * @param index the slot location
+ * @param includeRest whether to include the rest
+ * @return the bytes in the given slot
+ */
+ public static byte[] getSlot(byte[] bytes, int index, boolean includeRest) {
+ // Find the starting index
+ byte delimiter = SEPARATOR_BYTE;
+ int start;
+ if (index == 0) {
+ // make start -1 so that end determination will start at offset 0.
+ start = -1;
+ } else {
+ start = getOffsetOfDelimeter(bytes, delimiter, 0, index);
+ if (start == -1) return null;
+ }
+ // Find the ending index
+ int end = getOffsetOfDelimeter(bytes, delimiter, start + 1, 1);
+ // Calculate the length
+ int length;
+ if (end == -1 || includeRest) {
+ length = bytes.length - start - 1;
+ } else {
+ length = end - start - 1;
+ }
+ byte[] result = new byte[length];
+ System.arraycopy(bytes, start + 1, result, 0, length);
+ return result;
+ }
+
+ public static byte[] toBytes(String[] slots) {
+ StringBuilder buffer = new StringBuilder();
+ for (int i = 0; i < slots.length; i++) {
+ String string = slots[i];
+ buffer.append(string);
+ buffer.append(new String(new byte[] {SyncByteConverter.SEPARATOR_BYTE }));
+ }
+ return buffer.toString().getBytes();
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoStatistics.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoStatistics.java
new file mode 100644
index 000000000..5cdb14617
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoStatistics.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.eclipse.team.core.synchronize.SyncInfo;
+
+/**
+ * Counts SyncInfo states and allows for easy querying for different sync states.
+ */
+public class SyncInfoStatistics {
+ // {int sync kind -> int number of infos with that sync kind in this sync set}
+ protected Map<Integer, Long> stats = new HashMap<>();
+
+ /**
+ * Count this sync kind. Only the type of the sync info is stored.
+ * @param info the new info
+ */
+ public void add(SyncInfo info) {
+ // update statistics
+ Long count = stats.get(Integer.valueOf(info.getKind()));
+ if(count == null) {
+ count = Long.valueOf(0);
+ }
+ stats.put(Integer.valueOf(info.getKind()), Long.valueOf(count.longValue() + 1));
+ }
+
+ /**
+ * Remove this sync kind.
+ * @param info the info type to remove
+ */
+ public void remove(SyncInfo info) {
+ // update stats
+ Integer kind = Integer.valueOf(info.getKind());
+ Long count = stats.get(kind);
+ if(count == null) {
+ // error condition, shouldn't be removing if we haven't added yet
+ // programmer error calling remove before add.
+ } else {
+ long newCount = count.intValue() - 1;
+ if(newCount > 0) {
+ stats.put(kind, Long.valueOf(newCount));
+ } else {
+ stats.remove(kind);
+ }
+ }
+ }
+
+ /**
+ * Return the count of sync infos for the specified sync kind. A mask can be used to acucmulate
+ * counts for specific directions or change types.
+ * To return the number of outgoing changes:
+ * long outgoingChanges = stats.countFor(SyncInfo.OUTGOING, SyncInfo.DIRECTION_MASK);
+ *
+ * @param kind the sync kind for which to return the count
+ * @param mask the mask applied to the stored sync kind
+ * @return the number of sync info types added for the specific kind
+ */
+ public long countFor(int kind, int mask) {
+ if(mask == 0) {
+ Long count = stats.get(Integer.valueOf(kind));
+ return count == null ? 0 : count.longValue();
+ } else {
+ Iterator it = stats.keySet().iterator();
+ long count = 0;
+ while (it.hasNext()) {
+ Integer key = (Integer) it.next();
+ if((key.intValue() & mask) == kind) {
+ count += stats.get(key).intValue();
+ }
+ }
+ return count;
+ }
+ }
+
+ /**
+ * Clear the statistics counts. All calls to countFor() will return 0 until new
+ * sync infos are added.
+ */
+ public void clear() {
+ stats.clear();
+ }
+
+ /**
+ * For debugging
+ */
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder();
+ Iterator it = stats.keySet().iterator();
+ while (it.hasNext()) {
+ Integer kind = (Integer) it.next();
+ out.append(SyncInfo.kindToString(kind.intValue()) + ": " + stats.get(kind) + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ return out.toString();
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoTreeChangeEvent.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoTreeChangeEvent.java
new file mode 100644
index 000000000..5ec9951db
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoTreeChangeEvent.java
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.team.core.synchronize.*;
+
+public class SyncInfoTreeChangeEvent extends SyncInfoSetChangeEvent implements ISyncInfoTreeChangeEvent {
+
+ private Set<IResource> removedSubtrees = new HashSet<>();
+ private Set<IResource> addedSubtrees = new HashSet<>();
+
+ public SyncInfoTreeChangeEvent(SyncInfoSet set) {
+ super(set);
+ }
+
+ public void removedSubtreeRoot(IResource root) {
+ if (addedSubtrees.contains(root)) {
+ // The root was added and removed which is a no-op
+ addedSubtrees.remove(root);
+ } else if (isDescendantOfAddedRoot(root)) {
+ // Nothing needs to be done since no listeners ever knew about the root
+ } else {
+ // check if the root is a child of an existing root
+ // (in which case it need not be added).
+ // Also, remove any exisiting roots that are children
+ // of the new root
+ for (Iterator<IResource> iter = removedSubtrees.iterator(); iter.hasNext();) {
+ IResource element = iter.next();
+ // check if the root is already in the list
+ if (root.equals(element)) return;
+ if (isParent(root, element)) {
+ // the root invalidates the current element
+ iter.remove();
+ } else if (isParent(element, root)) {
+ // the root is a child of an existing element
+ return;
+ }
+ }
+ removedSubtrees.add(root);
+ }
+ }
+
+ private boolean isParent(IResource root, IResource element) {
+ return root.getFullPath().isPrefixOf(element.getFullPath());
+ }
+
+ public void addedSubtreeRoot(IResource parent) {
+ if (removedSubtrees.contains(parent)) {
+ // The root was re-added. Just removing the removedRoot
+ // may not give the proper event.
+ // Since we can't be sure, just force a reset.
+ reset();
+ } else {
+ // only add the root if their isn't a higher root in the list already
+ if (!isDescendantOfAddedRoot(parent)) {
+ addedSubtrees.add(parent);
+ }
+ }
+ }
+
+ private boolean isDescendantOfAddedRoot(IResource resource) {
+ for (Iterator iter = addedSubtrees.iterator(); iter.hasNext();) {
+ IResource root = (IResource) iter.next();
+ if (isParent(root, resource)) {
+ // There is a higher added root already in the list
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public IResource[] getAddedSubtreeRoots() {
+ return addedSubtrees.toArray(new IResource[addedSubtrees.size()]);
+ }
+
+ @Override
+ public IResource[] getRemovedSubtreeRoots() {
+ return removedSubtrees.toArray(new IResource[removedSubtrees.size()]);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return super.isEmpty() && removedSubtrees.isEmpty() && addedSubtrees.isEmpty();
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoWorkingSetFilter.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoWorkingSetFilter.java
new file mode 100644
index 000000000..803366628
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncInfoWorkingSetFilter.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import java.util.*;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.subscribers.*;
+import org.eclipse.team.core.synchronize.*;
+import org.eclipse.team.internal.core.TeamPlugin;
+
+/**
+ * WorkingSet filter for a SyncSet.
+ */
+public class SyncInfoWorkingSetFilter extends FastSyncInfoFilter {
+
+ private IResource[] resources;
+
+ @Override
+ public boolean select(SyncInfo info) {
+ // if there's no set, the resource is included
+ if (isEmpty()) return true;
+ return isIncluded(info.getLocal());
+ }
+
+ /*
+ * Answer true if the given resource is included in the working set
+ */
+ private boolean isIncluded(IResource resource) {
+ // otherwise, if their is a parent of the resource in the set,
+ // it is included
+ for (int i = 0; i < resources.length; i++) {
+ IResource setResource = resources[i];
+ if (isParent(setResource, resource)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isParent(IResource parent, IResource child) {
+ return (parent.getFullPath().isPrefixOf(child.getFullPath()));
+ }
+
+ public IResource[] getRoots(Subscriber subscriber) {
+ IResource[] roots = subscriber.roots();
+ if (isEmpty()) return roots;
+
+ // filter the roots by the selected working set
+ Set<IResource> result = new HashSet<>();
+ for (int i = 0; i < roots.length; i++) {
+ IResource resource = roots[i];
+ result.addAll(Arrays.asList(getIntersectionWithSet(subscriber, resource)));
+ }
+ return result.toArray(new IResource[result.size()]);
+ }
+
+ /*
+ * Answer the intersection between the given resource and it's children
+ * and the receiver's working set.
+ */
+ private IResource[] getIntersectionWithSet(Subscriber subscriber, IResource resource) {
+ List<IResource> result = new ArrayList<>();
+ for (int i = 0; i < resources.length; i++) {
+ IResource setResource = resources[i];
+ if (setResource != null) {
+ if (isParent(resource, setResource)) {
+ try {
+ if (subscriber.isSupervised(setResource)) {
+ result.add(setResource);
+ }
+ } catch (TeamException e) {
+ // Log the exception and add the resource to the list
+ TeamPlugin.log(e);
+ result.add(setResource);
+ }
+ } else if (isParent(setResource, resource)) {
+ result.add(resource);
+ }
+ }
+ }
+ return result.toArray(new IResource[result.size()]);
+ }
+
+ public void setWorkingSet(IResource[] resources) {
+ this.resources = resources;
+ }
+
+ public IResource[] getWorkingSet() {
+ return this.resources;
+ }
+
+ private boolean isEmpty() {
+ return resources == null || resources.length == 0;
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInput.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInput.java
new file mode 100644
index 000000000..d463fe62e
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInput.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2006 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.synchronize.*;
+import org.eclipse.team.internal.core.Policy;
+
+/**
+ * This is the superclass for all SyncSet input providers
+ */
+public abstract class SyncSetInput {
+
+ private SubscriberSyncInfoSet syncSet;
+ private SyncInfoFilter filter = new FastSyncInfoFilter();
+
+ public SyncSetInput(SubscriberEventHandler handler) {
+ syncSet = new SubscriberSyncInfoSet(handler);
+ }
+
+ public SubscriberSyncInfoSet getSyncSet() {
+ return syncSet;
+ }
+
+ /**
+ * This method is invoked from reset to get all the sync information from
+ * the input source.
+ */
+ protected abstract void fetchInput(IProgressMonitor monitor) throws TeamException;
+
+ /**
+ * The input is no longer being used. Disconnect it from its source.
+ */
+ public abstract void disconnect();
+
+ /**
+ * Reset the input. This will clear the current contents of the sync set and
+ * obtain the contents from the input source.
+ */
+ public void reset(IProgressMonitor monitor) throws TeamException {
+
+ try {
+ syncSet.beginInput();
+ monitor = Policy.monitorFor(monitor);
+ monitor.beginTask(null, 100);
+ syncSet.clear();
+ fetchInput(Policy.subMonitorFor(monitor, 90));
+ } finally {
+ syncSet.endInput(Policy.subMonitorFor(monitor, 10));
+ monitor.done();
+ }
+ }
+
+ /**
+ * Collect the change in the provided sync info.
+ */
+ protected void collect(SyncInfo info, IProgressMonitor monitor) {
+ boolean isOutOfSync = filter.select(info, monitor);
+ SyncInfo oldInfo = syncSet.getSyncInfo(info.getLocal());
+ boolean wasOutOfSync = oldInfo != null;
+ if (isOutOfSync) {
+ syncSet.add(info);
+ } else if (wasOutOfSync) {
+ syncSet.remove(info.getLocal());
+ }
+ }
+
+ protected void remove(IResource resource) {
+ SyncInfo oldInfo = syncSet.getSyncInfo(resource);
+ if (oldInfo != null) {
+ syncSet.remove(resource);
+ }
+ }
+
+ public SyncInfoFilter getFilter() {
+ return filter;
+ }
+
+ public void setFilter(SyncInfoFilter filter) {
+ this.filter = filter;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInputFromSubscriber.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInputFromSubscriber.java
new file mode 100644
index 000000000..24baae3ec
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInputFromSubscriber.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.ITeamStatus;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.subscribers.Subscriber;
+
+/**
+ * Records resource synchronization changes from a Team subscriber. The actual changes
+ * are calculated via the SubscriberEventHandler and stored in this input.
+ */
+public class SyncSetInputFromSubscriber extends SyncSetInput {
+
+ private Subscriber subscriber;
+
+ public SyncSetInputFromSubscriber(Subscriber subscriber, SubscriberEventHandler handler) {
+ super(handler);
+ this.subscriber = subscriber;
+ }
+
+ @Override
+ public void disconnect() {
+ }
+
+ public Subscriber getSubscriber() {
+ return subscriber;
+ }
+
+ @Override
+ protected void fetchInput(IProgressMonitor monitor) throws TeamException {
+ // don't calculate changes. The SubscriberEventHandler will fetch the
+ // input in a job and update this sync set when the changes are
+ // calculated.
+ }
+
+ /**
+ * Handle an error that occurred while populating the receiver's set.
+ * The <code>ITeamStatus</code> includes the resource for which the
+ * error occurred.
+ * This error is propogated to any set listeners.
+ * @param status the error status
+ */
+ public void handleError(ITeamStatus status) {
+ getSyncSet().addError(status);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInputFromSyncSet.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInputFromSyncSet.java
new file mode 100644
index 000000000..57c1044fe
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/SyncSetInputFromSyncSet.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.ITeamStatus;
+import org.eclipse.team.core.synchronize.*;
+import org.eclipse.team.internal.core.Policy;
+
+/**
+ * This class uses the contents of one sync set as the input of another.
+ */
+public class SyncSetInputFromSyncSet extends SyncSetInput implements ISyncInfoSetChangeListener {
+
+ SubscriberSyncInfoSet inputSyncSet;
+
+ public SyncSetInputFromSyncSet(SubscriberSyncInfoSet set, SubscriberEventHandler handler) {
+ super(handler);
+ this.inputSyncSet = set;
+ inputSyncSet.addSyncSetChangedListener(this);
+ }
+
+ @Override
+ public void disconnect() {
+ if (inputSyncSet == null) return;
+ inputSyncSet.removeSyncSetChangedListener(this);
+ inputSyncSet = null;
+ }
+
+ @Override
+ protected void fetchInput(IProgressMonitor monitor) {
+ if (inputSyncSet == null) return;
+ SyncInfo[] infos = inputSyncSet.getSyncInfos();
+ for (int i = 0; i < infos.length; i++) {
+ collect(infos[i], monitor);
+ }
+ }
+
+ @Override
+ public void syncInfoChanged(ISyncInfoSetChangeEvent event, IProgressMonitor monitor) {
+ SyncInfoSet syncSet = getSyncSet();
+ try {
+ syncSet.beginInput();
+ monitor.beginTask(null, 105);
+ syncSetChanged(event.getChangedResources(), Policy.subMonitorFor(monitor, 50));
+ syncSetChanged(event.getAddedResources(), Policy.subMonitorFor(monitor, 50));
+ remove(event.getRemovedResources());
+ } finally {
+ syncSet.endInput(Policy.subMonitorFor(monitor, 5));
+ }
+ }
+
+ private void syncSetChanged(SyncInfo[] infos, IProgressMonitor monitor) {
+ for (int i = 0; i < infos.length; i++) {
+ collect(infos[i], monitor);
+ }
+ }
+
+ private void remove(IResource[] resources) {
+ for (int i = 0; i < resources.length; i++) {
+ remove(resources[i]);
+ }
+ }
+
+ public void reset() {
+ inputSyncSet.connect(this);
+ }
+
+ @Override
+ public void syncInfoSetReset(SyncInfoSet set, IProgressMonitor monitor) {
+ if(inputSyncSet == null) {
+ set.removeSyncSetChangedListener(this);
+ } else {
+ SyncInfoSet syncSet = getSyncSet();
+ try {
+ syncSet.beginInput();
+ monitor.beginTask(null, 100);
+ syncSet.clear();
+ fetchInput(Policy.subMonitorFor(monitor, 95));
+ } finally {
+ syncSet.endInput(Policy.subMonitorFor(monitor, 5));
+ monitor.done();
+ }
+ }
+ }
+
+ @Override
+ public void syncInfoSetErrors(SyncInfoSet set, ITeamStatus[] errors, IProgressMonitor monitor) {
+ SubscriberSyncInfoSet syncSet = getSyncSet();
+ try {
+ syncSet.beginInput();
+ for (int i = 0; i < errors.length; i++) {
+ ITeamStatus status = errors[i];
+ syncSet.addError(status);
+ }
+ } finally {
+ syncSet.endInput(monitor);
+ }
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ThreeWayBaseTree.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ThreeWayBaseTree.java
new file mode 100644
index 000000000..7b1ec07de
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/ThreeWayBaseTree.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.TeamException;
+import org.eclipse.team.core.variants.IResourceVariant;
+import org.eclipse.team.core.variants.ResourceVariantByteStore;
+import org.eclipse.team.core.variants.ResourceVariantTree;
+import org.eclipse.team.core.variants.ThreeWaySubscriber;
+
+/**
+ * Allow access to the base resource variants but do not support refresh
+ * or modification.
+ */
+public final class ThreeWayBaseTree extends ResourceVariantTree {
+
+ private ThreeWaySubscriber subscriber;
+
+ /*
+ * A resource variant byte store that accesses the base bytes from a three-way
+ * synchronizer. The modification methods are disabled as the base should
+ * only be modified in the synchronizer directly.
+ */
+ static class BaseResourceVariantByteStore extends ResourceVariantByteStore {
+ private ThreeWaySubscriber subscriber;
+ public BaseResourceVariantByteStore(ThreeWaySubscriber subscriber) {
+ this.subscriber = subscriber;
+ }
+ @Override
+ public void dispose() {
+ // Nothing to do
+ }
+ @Override
+ public byte[] getBytes(IResource resource) throws TeamException {
+ return subscriber.getSynchronizer().getBaseBytes(resource);
+ }
+ @Override
+ public boolean setBytes(IResource resource, byte[] bytes) throws TeamException {
+ // Base bytes are set directly in the synchronizer
+ return false;
+ }
+ @Override
+ public boolean flushBytes(IResource resource, int depth) throws TeamException {
+ // Base bytes are flushed directly in the synchronizer
+ return false;
+ }
+ @Override
+ public boolean deleteBytes(IResource resource) throws TeamException {
+ // Base bytes are deleted directly in the synchronizer
+ return false;
+ }
+ @Override
+ public IResource[] members(IResource resource) throws TeamException {
+ return subscriber.getSynchronizer().members(resource);
+ }
+ }
+
+ /**
+ * Create a base resource variant tree that accesses the base bytes
+ * from a three-way synchronizer.
+ * @param subscriber the three-way subscriber
+ */
+ public ThreeWayBaseTree(ThreeWaySubscriber subscriber) {
+ super(new BaseResourceVariantByteStore(subscriber));
+ this.subscriber = subscriber;
+ }
+
+ @Override
+ public IResource[] refresh(IResource[] resources, int depth, IProgressMonitor monitor) throws TeamException {
+ return new IResource[0];
+ }
+
+ @Override
+ protected IResourceVariant[] fetchMembers(IResourceVariant variant, IProgressMonitor progress) throws TeamException {
+ // Refresh not supported
+ return new IResourceVariant[0];
+ }
+
+ @Override
+ protected IResourceVariant fetchVariant(IResource resource, int depth, IProgressMonitor monitor) throws TeamException {
+ // Refresh not supported
+ return null;
+ }
+
+ @Override
+ public IResource[] roots() {
+ return getSubscriber().roots();
+ }
+
+ @Override
+ public IResourceVariant getResourceVariant(IResource resource) throws TeamException {
+ return getSubscriber().getResourceVariant(resource, getByteStore().getBytes(resource));
+ }
+
+ private ThreeWaySubscriber getSubscriber() {
+ return subscriber;
+ }
+
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/WorkingSetFilteredSyncInfoCollector.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/WorkingSetFilteredSyncInfoCollector.java
new file mode 100644
index 000000000..baf8cffe1
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/WorkingSetFilteredSyncInfoCollector.java
@@ -0,0 +1,127 @@
+/*******************************************************************************
+ * 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRunnable;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.team.core.synchronize.*;
+
+/**
+ * This collector maintains a {@link SyncInfoSet} for a particular team subscriber keeping
+ * it up-to-date with both incoming changes and outgoing changes as they occur for
+ * resources in the workspace. The collector can be configured to consider all the subscriber's
+ * roots or only a subset.
+ * <p>
+ * The advantage of this collector is that it processes both resource and team
+ * subscriber deltas in a background thread.
+ * </p>
+ * @since 3.0
+ */
+public final class WorkingSetFilteredSyncInfoCollector {
+
+ private WorkingSetSyncSetInput workingSetInput;
+ private SyncSetInputFromSyncSet filteredInput;
+ private SubscriberEventHandler eventHandler;
+
+ /**
+ * Create a collector that collects out-of-sync resources that are children of
+ * the given roots. If the roots are <code>null</code>, then all out-of-sync resources
+ * from the subscriber are collected. An empty array of roots will cause no resources
+ * to be collected. The <code>start()</code> method must be called after creation
+ * to rpime the collector's sync sets.
+ * @param collector the subscriber's collector
+ * @param roots the roots of the out-of-sync resources to be collected
+ */
+ public WorkingSetFilteredSyncInfoCollector(SubscriberSyncInfoCollector collector, IResource[] roots) {
+ this.eventHandler = collector.getEventHandler();
+ // TODO: optimize and don't use working set if no roots are passed in
+ workingSetInput = new WorkingSetSyncSetInput((SubscriberSyncInfoSet)collector.getSyncInfoSet(), getEventHandler());
+ filteredInput = new SyncSetInputFromSyncSet(workingSetInput.getSyncSet(), getEventHandler());
+ filteredInput.setFilter(new SyncInfoFilter() {
+ @Override
+ public boolean select(SyncInfo info, IProgressMonitor monitor) {
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Return the set that provides access to the out-of-sync resources for the collector's
+ * subscriber that are descendants of the roots for the collector,
+ * are in the collector's working set and match the collectors filter.
+ * @return a SyncInfoSet containing out-of-sync resources
+ */
+ public SyncInfoTree getSyncInfoTree() {
+ return filteredInput.getSyncSet();
+ }
+
+ /**
+ * Clears this collector's sync info sets and causes them to be recreated from the
+ * associated <code>Subscriber</code>. The reset will occur in the background. If the
+ * caller wishes to wait for the reset to complete, they should call
+ * waitForCollector(IProgressMonitor).
+ */
+ public void reset() {
+ workingSetInput.reset();
+ }
+
+ /**
+ * Disposes of the background job associated with this collector and deregisters
+ * all it's listeners. This method must be called when the collector is no longer
+ * referenced and could be garbage collected.
+ */
+ public void dispose() {
+ workingSetInput.disconnect();
+ if(filteredInput != null) {
+ filteredInput.disconnect();
+ }
+ }
+
+ /**
+ * Return the event handler that performs the background processing for this collector.
+ * The event handler also serves the purpose of serializing the modifications and adjustments
+ * to the collector's sync sets in order to ensure that the state of the sets is kept
+ * consistent.
+ * @return Returns the eventHandler.
+ */
+ protected SubscriberEventHandler getEventHandler() {
+ return eventHandler;
+ }
+
+ /**
+ * Set the filter for this collector. Only elements that match the filter will
+ * be in the out sync info set.
+ * @param filter the sync info filter
+ */
+ public void setFilter(SyncInfoFilter filter) {
+ filteredInput.setFilter(filter);
+ filteredInput.reset();
+ }
+
+ /**
+ * Return a <code>SyncInfoSet</code> that contains the out-of-sync elements
+ * from the subsciber sync info set filtered
+ * by the working set resources but not the collector's <code>SyncInfoFilter</code>.
+ * @return a <code>SyncInfoSet</code>
+ */
+ public SyncInfoSet getWorkingSetSyncInfoSet() {
+ return workingSetInput.getSyncSet();
+ }
+
+ /**
+ * Run the given runnable in the event handler of the collector
+ * @param runnable a runnable
+ */
+ public void run(IWorkspaceRunnable runnable) {
+ eventHandler.run(runnable, true /* front of queue */);
+ }
+}
diff --git a/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/WorkingSetSyncSetInput.java b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/WorkingSetSyncSetInput.java
new file mode 100644
index 000000000..a15b37616
--- /dev/null
+++ b/bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core/subscribers/WorkingSetSyncSetInput.java
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2006 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.internal.core.subscribers;
+
+import org.eclipse.core.resources.IResource;
+
+public class WorkingSetSyncSetInput extends SyncSetInputFromSyncSet {
+
+ private SyncInfoWorkingSetFilter workingSetFilter = new SyncInfoWorkingSetFilter();
+
+ public WorkingSetSyncSetInput(SubscriberSyncInfoSet set, SubscriberEventHandler handler) {
+ super(set, handler);
+ setFilter(workingSetFilter);
+ }
+
+ public void setWorkingSet(IResource[] resources) {
+ workingSetFilter.setWorkingSet(resources);
+ }
+
+ public IResource[] getWorkingSet() {
+ return workingSetFilter.getWorkingSet();
+ }
+}

Back to the top