diff options
Diffstat (limited to 'bundles/org.eclipse.team.core/src/org/eclipse/team/internal/core')
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(); + } +} |