blob: 79355152be5186424672792b1c6d30e01bc9536b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* 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.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
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.BackgroundEventHandler;
import org.eclipse.team.internal.core.Messages;
import org.eclipse.team.internal.core.Policy;
/**
* 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);
}
}