| /******************************************************************************* |
| * Copyright (c) 2010, 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.wst.sse.core.indexing; |
| |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IResourceChangeEvent; |
| import org.eclipse.core.resources.IResourceChangeListener; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.resources.IResourceDeltaVisitor; |
| import org.eclipse.core.resources.IResourceProxy; |
| import org.eclipse.core.resources.IResourceProxyVisitor; |
| import org.eclipse.core.resources.ISaveParticipant; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.ISafeRunnable; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.core.runtime.SafeRunner; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.wst.sse.core.internal.Logger; |
| |
| /** |
| * <p> |
| * A generic class for implementing a resource index manager. It is important |
| * to note that this only provides the framework for managing an index, not |
| * actually indexing. The subtle difference is that the manager is in charge |
| * of paying attention to all of the resource actions that take place in the |
| * workspace and filtering those actions down to simple actions that need to |
| * be performed on whatever index this manager is managing. |
| * </p> |
| * |
| * <p> |
| * The manager does its very best to make sure the index is always consistent, |
| * even if resource events take place when the manager is not running. In the |
| * event that the manager determines it has missed, lost, or corrupted any |
| * resource change events that have occurred before, during, or after its |
| * activation or deactivation then the manager will inspect the entire |
| * workspace to insure the index it is managing is consistent. |
| * </p> |
| * |
| * <p> |
| * This class and its contents are <b>PROVISIONAL</b>. They can and very likely will |
| * change prior to finalization in the next main release. |
| * </p> |
| * |
| */ |
| public abstract class AbstractIndexManager { |
| |
| /** Used to encode path bytes in case they contain double byte characters */ |
| private static final String ENCODING_UTF16 = "utf16"; //$NON-NLS-1$ |
| |
| /** Default time to wait for other tasks to finish */ |
| private static final int WAIT_TIME = 300; |
| |
| /** |
| * <p>Used to report progress on jobs where the total work to complete is unknown. |
| * The created effect is a progress bar that moves but that will never complete.</p> |
| */ |
| private static final int UNKNOWN_WORK = 100; |
| |
| /** The amount of events to batch up before sending them off to the processing job*/ |
| private static final int BATCH_UP_AMOUNT = 100; |
| |
| /** If this file exists then a full workspace re-processing is needed */ |
| private static final String RE_PROCESS_FILE_NAME = ".re-process"; //$NON-NLS-1$ |
| |
| /** Common error message to log */ |
| private static final String LOG_ERROR_INDEX_INVALID = |
| "Index may become invalid, incomplete, or enter some other inconsistent state."; //$NON-NLS-1$ |
| |
| /** State: manager is stopped */ |
| private static final byte STATE_DISABLED = 0; |
| |
| /** State: manager is running */ |
| private static final byte STATE_ENABLED = 1; |
| |
| /** Action: add to index */ |
| protected static final byte ACTION_ADD = 0; |
| |
| /** Action: remove from index */ |
| protected static final byte ACTION_REMOVE = 1; |
| |
| /** Action: add to index caused by move operation */ |
| protected static final byte ACTION_ADD_MOVE_FROM = 2; |
| |
| /** Action: remove from index caused by move operation */ |
| protected static final byte ACTION_REMOVE_MOVE_TO = 3; |
| |
| /** Source: action originated from resource change event */ |
| protected static final byte SOURCE_RESOURCE_CHANGE = 0; |
| |
| /** Source: action originated from workspace scan */ |
| protected static final byte SOURCE_WORKSPACE_SCAN = 1; |
| |
| /** Source: action originated from saved state */ |
| protected static final byte SOURCE_SAVED_STATE = 2; |
| |
| /** Source: preserved resources to index */ |
| protected static final byte SOURCE_PRESERVED_RESOURCES_TO_INDEX = 3; |
| |
| /** the name of this index manager */ |
| private String fName; |
| |
| /** Message to use when manager is running */ |
| private String fMessage_Running; |
| |
| /** Message to use when manager is initializing */ |
| private String fMessage_Initializing; |
| |
| /** |
| * <p>Message to use when manager is processing files. |
| * Should contain one parameter to be filled in with the number of files to process.</p> |
| */ |
| private String fMessage_ProcessingFiles; |
| |
| /** {@link IResourceChangeListener} to listen for file changes */ |
| private ResourceChangeListener fResourceChangeListener; |
| |
| /** The {@link Job} that does all of the indexing */ |
| private ResourceEventProcessingJob fResourceEventProcessingJob; |
| |
| /** A {@link Job} to search the workspace for all files */ |
| private Job fWorkspaceVisitorJob; |
| |
| /** |
| * <p>Current state of the manager</p> |
| * |
| * @see #STATE_DISABLED |
| * @see #STATE_ENABLED |
| */ |
| private volatile byte fState; |
| |
| /** used to prevent manager from starting and stopping at the same time */ |
| private Object fStartStopLock = new Object(); |
| |
| /** <code>true</code> if the manager is currently starting, <code>false</code> otherwise */ |
| private boolean fStarting; |
| |
| /** |
| * <p>Creates the manager with a given name.</p> |
| * |
| * @param name This will be pre-pended to progress reporting messages and thus should |
| * be translated |
| * |
| * @param messageRunning Message to use when manager is running. Should be user readable. |
| * @param messagegInitializing Message to use when manager is initializing. Should be user readable. |
| * @param messageProcessingFiles Message to use when manager is processing files. |
| * Should contain two parameters to be filled in with the number of files to process, and |
| * the file currently being processed. |
| * Should be user readable. |
| */ |
| |
| protected AbstractIndexManager(String name, String messageRunning, |
| String messagegInitializing, String messageProcessingFiles) { |
| |
| this.fMessage_Running = messageRunning; |
| this.fMessage_ProcessingFiles = messageProcessingFiles; |
| this.fMessage_Initializing = messagegInitializing; |
| |
| this.fName = name; |
| this.fState = STATE_DISABLED; |
| this.fResourceChangeListener = new ResourceChangeListener(); |
| this.fResourceEventProcessingJob = new ResourceEventProcessingJob(); |
| this.fStarting = false; |
| } |
| |
| /** |
| * <p>Starts up the {@link AbstractIndexManager}. If a {@link IResourceDelta} |
| * is provided then it is assumed that all other files in the workspace |
| * have already been index and thus only those in the provided |
| * {@link IResourceDelta} will be processed. Else if the provided |
| * {@link IResourceDelta} is <code>null</code> it is assumed no files |
| * have been indexed yet so the entire workspace will be searched for |
| * files to be indexed.</p> |
| * |
| * <p>If {@link IResourceDelta} is provided this will block until that delta |
| * has finished processing. If no {@link IResourceDelta} provided then a |
| * separate job will be created to process the entire workspace and this method |
| * will return without waiting for that job to complete</p> |
| * |
| * <p>Will block until {@link #stop()} has finished running if it is |
| * currently running</p> |
| * |
| * @param savedStateDelta the delta from a saved state, if <code>null</code> |
| * then the entire workspace will be searched for files to index, else |
| * only files in this {@link IResourceDelta} will be indexed |
| * @param monitor This action can not be canceled but this monitor will be used |
| * to report progress |
| */ |
| public final void start(IResourceDelta savedStateDelta, IProgressMonitor monitor) { |
| SubMonitor progress = SubMonitor.convert(monitor); |
| synchronized (this.fStartStopLock) { |
| this.fStarting = true; |
| |
| if(this.fState == STATE_DISABLED) { |
| //report status |
| progress.beginTask(this.fName, 2); |
| |
| //start listening for resource change events |
| this.fResourceChangeListener.start(); |
| |
| //check to see if a full re-index is required |
| boolean forcedFullReIndexNeeded = this.isForcedFullReIndexNeeded(); |
| |
| /* start the indexing job only loading preserved state if not doing full index |
| * if failed loading preserved state then force full re-index |
| */ |
| forcedFullReIndexNeeded = !this.fResourceEventProcessingJob.start(!forcedFullReIndexNeeded, |
| progress.newChild(1)); |
| progress.setWorkRemaining(1); |
| |
| //don't bother processing saved delta if forced full re-index is needed |
| if(!forcedFullReIndexNeeded) { |
| /* if there is a delta attempt to process it |
| * else need to do a full workspace index |
| */ |
| if(savedStateDelta != null) { |
| forcedFullReIndexNeeded = false; |
| try { |
| //deal with reporting progress |
| SubMonitor savedStateProgress = progress.newChild(1, SubMonitor.SUPPRESS_NONE); |
| |
| //process delta |
| ResourceDeltaVisitor visitor = new ResourceDeltaVisitor(savedStateProgress, |
| AbstractIndexManager.SOURCE_SAVED_STATE); |
| savedStateDelta.accept(visitor); |
| |
| //process any remaining batched up resources to index |
| visitor.processBatchedResourceEvents(); |
| } catch (CoreException e) { |
| forcedFullReIndexNeeded = true; |
| Logger.logException(this.fName + ": Could not process saved state. " + //$NON-NLS-1$ |
| "Forced to do a full workspace re-index.", e); //$NON-NLS-1$ |
| } |
| } else { |
| forcedFullReIndexNeeded = true; |
| } |
| } |
| progress.worked(1); |
| |
| //if need to process the entire workspace do so in another job |
| if(forcedFullReIndexNeeded){ |
| this.fWorkspaceVisitorJob = new WorkspaceVisitorJob(); |
| this.fWorkspaceVisitorJob.schedule(); |
| } |
| |
| //update state |
| this.fState = STATE_ENABLED; |
| } |
| this.fStarting = false; |
| } |
| } |
| |
| /** |
| * <p>Safely shuts down the manager.</p> |
| * |
| * <p>This will block until the {@link #start(IResourceDelta, IProgressMonitor)} has |
| * finished (if running). Also until the current resource event has finished being |
| * processed. Finally it will block until the events still to be processed by |
| * the processing job have been preserved to be processed on the next call to |
| * {@link #start(IResourceDelta, IProgressMonitor)}.</p> |
| * |
| * <p>If at any point during this shut down processes something goes wrong the |
| * manager will be sure that on the next call to {@link #start(IResourceDelta, IProgressMonitor)} |
| * the entire workspace will be re-processed.</p> |
| * |
| * @throws InterruptedException |
| */ |
| public final void stop() throws InterruptedException { |
| synchronized (this.fStartStopLock) { |
| if(this.fState != STATE_DISABLED) { |
| |
| //stop listening for events, and wait for the current event to finish |
| this.fResourceChangeListener.stop(); |
| |
| // if currently visiting entire workspace, give up and try again next load |
| boolean forceFullReIndexNextStart = false; |
| if(this.fWorkspaceVisitorJob != null) { |
| if (this.fWorkspaceVisitorJob.getState() != Job.NONE) { |
| this.fWorkspaceVisitorJob.cancel(); |
| |
| this.forceFullReIndexNextStart(); |
| forceFullReIndexNextStart = true; |
| } |
| } |
| |
| //stop the indexing job, only preserve if not already forcing a re-index |
| forceFullReIndexNextStart = !this.fResourceEventProcessingJob.stop(!forceFullReIndexNextStart); |
| |
| //if preserving failed, then force re-index |
| if(forceFullReIndexNextStart) { |
| this.forceFullReIndexNextStart(); |
| } |
| |
| //update status |
| this.fState = STATE_DISABLED; |
| } |
| } |
| } |
| |
| /** |
| * @return the name of this indexer |
| */ |
| protected String getName() { |
| return this.fName; |
| } |
| |
| /** |
| * <p>Should be called by a client of the index this manager manages before the index |
| * is accessed, assuming the client wants an index consistent with the latest |
| * resource changes.</p> |
| * |
| * <p>The supplied monitor will be used to supply user readable progress as the manager |
| * insures the index has been given all the latest resource events. This monitor |
| * may be canceled, but if it is the state of the index is not guaranteed to be |
| * consistent with the latest resource change events.</p> |
| * |
| * @param monitor Used to report user readable progress as the manager insures the |
| * index is consistent with the latest resource events. This monitor can be canceled |
| * to stop waiting for consistency but then no guaranty is made about the consistency |
| * of the index in relation to unprocessed resource changes |
| * |
| * @return <code>true</code> if the wait finished successfully and the manager is consistent, |
| * <code>false</code> otherwise, either an error occurred while waiting for the manager |
| * or the monitor was canceled |
| * |
| * @throws InterruptedException This can happen when waiting for other jobs |
| */ |
| public final boolean waitForConsistent(IProgressMonitor monitor) { |
| boolean success = true; |
| boolean interupted = false; |
| SubMonitor progress = SubMonitor.convert(monitor); |
| |
| //set up the progress of waiting |
| int remainingWork = 4; |
| progress.beginTask(this.fMessage_Running,remainingWork); |
| |
| //wait for start up |
| if(this.fStarting && !monitor.isCanceled()) { |
| SubMonitor startingProgress = progress.newChild(1); |
| startingProgress.subTask(this.fMessage_Initializing); |
| while(this.fStarting && !monitor.isCanceled()) { |
| //this creates a never ending progress that still moves forward |
| startingProgress.setWorkRemaining(UNKNOWN_WORK); |
| startingProgress.newChild(1).worked(1); |
| try { |
| Thread.sleep(WAIT_TIME); |
| } catch (InterruptedException e) { |
| interupted = true; |
| } |
| } |
| } |
| progress.setWorkRemaining(--remainingWork); |
| |
| //wait for workspace visiting job |
| if(this.fWorkspaceVisitorJob != null && this.fWorkspaceVisitorJob.getState() != Job.NONE && !monitor.isCanceled()) { |
| SubMonitor workspaceVisitorProgress = progress.newChild(1); |
| workspaceVisitorProgress.subTask(this.fMessage_Initializing); |
| while(this.fWorkspaceVisitorJob.getState() != Job.NONE && !monitor.isCanceled()) { |
| //this creates a never ending progress that still moves forward |
| workspaceVisitorProgress.setWorkRemaining(UNKNOWN_WORK); |
| workspaceVisitorProgress.newChild(1).worked(1); |
| try { |
| Thread.sleep(WAIT_TIME); |
| } catch (InterruptedException e) { |
| interupted = true; |
| } |
| } |
| } |
| progress.setWorkRemaining(--remainingWork); |
| |
| //wait for the current resource event |
| if(this.fResourceChangeListener.isProcessingEvents() && !monitor.isCanceled()) { |
| SubMonitor workspaceVisitorProgress = progress.newChild(1); |
| workspaceVisitorProgress.subTask(this.fMessage_Running); |
| while(this.fResourceChangeListener.isProcessingEvents() && !monitor.isCanceled()) { |
| workspaceVisitorProgress.setWorkRemaining(UNKNOWN_WORK); |
| workspaceVisitorProgress.newChild(1).worked(1); |
| try { |
| this.fResourceChangeListener.waitForCurrentEvent(WAIT_TIME); |
| } catch (InterruptedException e) { |
| interupted = true; |
| } |
| } |
| } |
| progress.setWorkRemaining(--remainingWork); |
| |
| //wait for all files to be indexed |
| if(this.fResourceEventProcessingJob.getNumResourceEventsToProcess() != 0 && !monitor.isCanceled()) { |
| SubMonitor indexingProgress = progress.newChild(1); |
| int prevNumResrouces; |
| int numResources = this.fResourceEventProcessingJob.getNumResourceEventsToProcess(); |
| while(numResources != 0 && !monitor.isCanceled()) { |
| //update the progress indicator |
| indexingProgress.subTask(NLS.bind(this.fMessage_ProcessingFiles, "" + numResources)); //$NON-NLS-1$ |
| indexingProgress.setWorkRemaining(numResources); |
| prevNumResrouces = numResources; |
| numResources = this.fResourceEventProcessingJob.getNumResourceEventsToProcess(); |
| int numProcessed = prevNumResrouces - numResources; |
| indexingProgress.worked(numProcessed > 0 ? numProcessed : 0); |
| |
| //give the index some time to do some indexing |
| try { |
| this.fResourceEventProcessingJob.waitForConsistant(WAIT_TIME); |
| } catch (InterruptedException e) { |
| interupted = true; |
| } |
| } |
| } |
| progress.setWorkRemaining(--remainingWork); |
| |
| if(monitor.isCanceled()) { |
| success = false; |
| } |
| |
| //reset the interrupted flag if we were interrupted |
| if(interupted) { |
| Thread.currentThread().interrupt(); |
| } |
| |
| return success; |
| } |
| |
| /** |
| * <p>Called for each {@link IResource} given in a resource delta. If the resource |
| * type is a file then used to determine if that file should be processed by the manager, |
| * if the resource type is a project or directory then it is used to determine if the children |
| * of the project or directory should be processed looking for file resources.</p> |
| * |
| * <p><b>NOTE:</b> Even if <code>true</code> is returned for a directory resource that |
| * only means the children of the directory should be inspected for possible files to index. |
| * Directories themselves can not be managed in order to add to an index.</p> |
| * |
| * @param type the {@link IResource#getType()} result of the resource to possibly index |
| * @param path the full {@link IPath} to the resource to possibly index |
| * @return <code>true</code> If the resource with the given <code>type</code> and |
| * <code>path</code> should either itself be indexed, or its children should be indexed, |
| * <code>false</code> if neither the described resource or any children should be indexed |
| */ |
| protected abstract boolean isResourceToIndex(int type, IPath path); |
| |
| /** |
| * <p>Called for each {@link ResourceEvent} gathered by the various sources and processed |
| * by the {@link ResourceEventProcessingJob}. The implementation of this method |
| * should use the given information to update the index this manager is managing.</p> |
| * |
| * @param source The source that reported this resource event |
| * @param action The action to be taken on the given <code>resource</code> |
| * @param resource The index should perform the given <code>action</code> on this |
| * resource |
| * @param movePath If the given <code>action</code> is {@link AbstractIndexManager#ACTION_ADD_MOVE_FROM} |
| * or {@link AbstractIndexManager#ACTION_REMOVE_MOVE_TO} then this field will not be |
| * null and reports the path the given <code>resource</code> was either moved from or |
| * moved to respectively. |
| * |
| * @see AbstractIndexManager#SOURCE_RESOURCE_CHANGE |
| * @see AbstractIndexManager#SOURCE_SAVED_STATE |
| * @see AbstractIndexManager#SOURCE_WORKSPACE_SCAN |
| * @see AbstractIndexManager#SOURCE_PRESERVED_RESOURCES_TO_INDEX |
| * |
| * @see AbstractIndexManager#ACTION_ADD |
| * @see AbstractIndexManager#ACTION_REMOVE |
| * @see AbstractIndexManager#ACTION_ADD_MOVE_FROM |
| * @see AbstractIndexManager#ACTION_REMOVE_MOVE_TO |
| */ |
| protected abstract void performAction(byte source, byte action, IResource resource, |
| IPath movePath); |
| |
| /** |
| * <p>Gets the working location of the manager. This is where any relevant |
| * state can be persisted.</p> |
| * |
| * @return the working location of the manager |
| */ |
| protected abstract IPath getWorkingLocation(); |
| |
| /** |
| * <p>Next time the manager starts up force a full workspace index</p> |
| */ |
| private void forceFullReIndexNextStart() { |
| IPath reIndexPath = |
| AbstractIndexManager.this.getWorkingLocation().append(RE_PROCESS_FILE_NAME); |
| File file = new File(reIndexPath.toOSString()); |
| try { |
| file.createNewFile(); |
| } catch (IOException e) { |
| Logger.logException(this.fName + ": Could not create file to tell manager to" + //$NON-NLS-1$ |
| " do a full re-index on next load. " + //$NON-NLS-1$ |
| AbstractIndexManager.LOG_ERROR_INDEX_INVALID, e); |
| } |
| } |
| |
| /** |
| * @return <code>true</code> if a full workspace index is needed as dictated by |
| * a previous call to {@link #forceFullReIndexNextStart()}, <code>false</code> |
| * otherwise |
| */ |
| private boolean isForcedFullReIndexNeeded() { |
| boolean forcedFullReIndexNeeded = false; |
| IPath reIndexPath = |
| AbstractIndexManager.this.getWorkingLocation().append(RE_PROCESS_FILE_NAME); |
| File file = new File(reIndexPath.toOSString()); |
| if(file.exists()) { |
| file.delete(); |
| forcedFullReIndexNeeded = true; |
| } |
| |
| return forcedFullReIndexNeeded; |
| } |
| |
| /** |
| * <p>A system {@link Job} used to visit all of the files in the workspace |
| * looking for files to index.</p> |
| * |
| * <p>This should only have to be done once per workspace on the first load, |
| * but if it fails or a SavedState can not be retrieved on a subsequent |
| * workspace load then this will have to be done again.</p> |
| */ |
| private class WorkspaceVisitorJob extends Job { |
| /** |
| * <p>Default constructor that sets up this job as a system job</p> |
| */ |
| protected WorkspaceVisitorJob() { |
| super(AbstractIndexManager.this.fMessage_Initializing); |
| |
| this.setUser(false); |
| this.setSystem(true); |
| this.setPriority(Job.LONG); |
| } |
| |
| /** |
| * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor) |
| */ |
| protected IStatus run(IProgressMonitor monitor) { |
| try { |
| //update status |
| monitor.beginTask(AbstractIndexManager.this.fMessage_Initializing, |
| IProgressMonitor.UNKNOWN); |
| |
| //visit the workspace |
| WorkspaceVisitor visitor = new WorkspaceVisitor(monitor); |
| ResourcesPlugin.getWorkspace().getRoot().accept(visitor, IResource.NONE); |
| |
| //process any remaining batched up resources to index |
| visitor.processBatchedResourceEvents(); |
| } catch(CoreException e) { |
| Logger.logException(AbstractIndexManager.this.fName + |
| ": Failed visiting entire workspace for initial index. " + //$NON-NLS-1$ |
| AbstractIndexManager.LOG_ERROR_INDEX_INVALID, e); |
| } |
| |
| IStatus status; |
| if(monitor.isCanceled()) { |
| status = Status.CANCEL_STATUS; |
| } else { |
| status = Status.OK_STATUS; |
| } |
| |
| return status; |
| } |
| |
| /** |
| * <p>An {@link IResourceProxyVisitor} used to visit all of the files in the |
| * workspace looking for files to add to the index.</p> |
| * |
| * <p><b>NOTE: </b>After this visitor is used {@link WorkspaceVisitor#processBatchedResourceEvents() |
| * must be called to flush out the last of the {@link ResourceEvent}s produced |
| * by this visitor.</p> |
| */ |
| private class WorkspaceVisitor implements IResourceProxyVisitor { |
| /** {@link IProgressMonitor} used to report status and check for cancellation */ |
| private SubMonitor fProgress; |
| |
| /** |
| * {@link Map}<{@link IResource}, {@link ResourceEvent}> |
| * <p>Map of resources events created and batched up by this visitor. |
| * These events are periodical be sent off to the |
| * {@link ResourceEventProcessingJob} but need to be sent off |
| * one final time after this visitor finishes it work.</p> |
| * |
| * @see #processBatchedResourceEvents() |
| */ |
| private Map fBatchedResourceEvents; |
| |
| /** |
| * <p>Default constructor</p> |
| * @param monitor used to report status and allow this visitor to be canceled |
| */ |
| protected WorkspaceVisitor(IProgressMonitor monitor) { |
| this.fProgress = SubMonitor.convert(monitor); |
| this.fBatchedResourceEvents = new LinkedHashMap(BATCH_UP_AMOUNT); |
| } |
| |
| /** |
| * <p>As long as the monitor is not canceled visit each file in the workspace |
| * that should be visited.</p> |
| * |
| * @see org.eclipse.core.resources.IResourceProxyVisitor#visit(org.eclipse.core.resources.IResourceProxy) |
| * @see AbstractIndexManager#shouldVisit(String) |
| */ |
| public boolean visit(IResourceProxy proxy) throws CoreException { |
| this.fProgress.subTask(proxy.getName()); |
| |
| boolean visitChildren = false; |
| |
| /* if not canceled or a hidden resource then process file |
| * else don't visit children |
| */ |
| if(!this.fProgress.isCanceled()) { |
| /* if root node always visit children |
| * else ask manager implementation if resource and its children should be visited |
| */ |
| IPath path = proxy.requestFullPath(); |
| if(path.toString().equals("/")) { //$NON-NLS-1$ |
| visitChildren = true; |
| } else if(isResourceToIndex(proxy.getType(), path)) { |
| if(proxy.getType() == IResource.FILE) { |
| //add the file to be indexed |
| IFile file = (IFile) proxy.requestResource(); |
| if(file.exists()) { |
| this.fBatchedResourceEvents.put(file, new ResourceEvent( |
| AbstractIndexManager.SOURCE_WORKSPACE_SCAN, |
| AbstractIndexManager.ACTION_ADD, |
| null)); |
| } |
| } |
| visitChildren = true; |
| } |
| } |
| |
| //batch up resource changes before sending them out |
| if(this.fBatchedResourceEvents.size() >= BATCH_UP_AMOUNT) { |
| this.processBatchedResourceEvents(); |
| } |
| |
| return visitChildren; |
| } |
| |
| /** |
| * <p>Sends any batched up resource events created by this visitor to the |
| * {@link ResourceEventProcessingJob}.<p> |
| * |
| * <p><b>NOTE:</b> This will be called every so often as the visitor is |
| * visiting resources but needs to be called a final time by the user of |
| * this visitor to be sure the final events are sent off</p> |
| */ |
| protected void processBatchedResourceEvents() { |
| AbstractIndexManager.this.fResourceEventProcessingJob.addResourceEvents( |
| this.fBatchedResourceEvents); |
| this.fBatchedResourceEvents.clear(); |
| } |
| } |
| } |
| |
| /** |
| * <p>Used to listen to resource change events in the workspace. These events |
| * are batched up and then passed onto the {@link ResourceEventProcessingJob}.</p> |
| */ |
| private class ResourceChangeListener implements IResourceChangeListener { |
| /** |
| * <p>The number of events currently being processed by this listener.</p> |
| * <p>Use the {@link #fEventsBeingProcessedLock} when reading or writing this field</p> |
| * |
| * @see #fEventsBeingProcessedLock |
| */ |
| private volatile int fEventsBeingProcessed; |
| |
| /** |
| * Lock to use when reading or writing {@link #fEventsBeingProcessed} |
| * |
| * @see #fEventsBeingProcessed |
| */ |
| private final Object fEventsBeingProcessedLock = new Object(); |
| |
| /** |
| * <p>Current state of this listener</p> |
| * |
| * @see AbstractIndexManager#STATE_DISABLED |
| * @see AbstractIndexManager#STATE_ENABLED |
| */ |
| private volatile byte fState; |
| |
| /** |
| * <p>Default constructor</p> |
| */ |
| protected ResourceChangeListener() { |
| this.fState = STATE_DISABLED; |
| this.fEventsBeingProcessed = 0; |
| } |
| |
| /** |
| * <p>Start listening for resource change events</p> |
| */ |
| protected void start() { |
| this.fState = STATE_ENABLED; |
| ResourcesPlugin.getWorkspace().addResourceChangeListener(this); |
| } |
| |
| /** |
| * <p>Stop listening for resource change events and if already processing |
| * an event then wait for that processing to finish</p> |
| * |
| * @throws InterruptedException waiting for a current event to finish processing |
| * could be interrupted |
| */ |
| protected void stop() throws InterruptedException { |
| ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); |
| |
| //wait indefinitely for current event to finish processing |
| this.waitForCurrentEvent(0); |
| |
| this.fState = STATE_DISABLED; |
| } |
| |
| /** |
| * <p>Blocks until either the current resource event has been processed or |
| * until the given timeout has passed.</p> |
| * |
| * @param timeout block until either this timeout elapses (0 means never to timeout) |
| * or the current resource change event finishes being processed |
| * |
| * @throws InterruptedException This can happen when waiting for a lock |
| */ |
| protected void waitForCurrentEvent(int timeout) throws InterruptedException { |
| synchronized (this.fEventsBeingProcessedLock) { |
| if(this.fEventsBeingProcessed != 0) { |
| this.fEventsBeingProcessedLock.wait(timeout); |
| } |
| } |
| } |
| |
| /** |
| * @return <code>true</code> if this listener is currently processing any |
| * events, <code>false</code> otherwise. |
| */ |
| protected boolean isProcessingEvents() { |
| return this.fEventsBeingProcessed != 0; |
| } |
| |
| /** |
| * <p>Process a resource change event. If it is a pre-close or pre-delete then |
| * the {@link ResourceEventProcessingJob} is paused so it does not try to |
| * process resources that are about to be deleted. The {@link ResourceDeltaVisitor} |
| * is used to actually process the event.</p> |
| * |
| * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent) |
| * @see ResourceDeltaVisitor |
| */ |
| public void resourceChanged(IResourceChangeEvent event) { |
| try { |
| //update the number of events being processed |
| synchronized (this.fEventsBeingProcessedLock) { |
| ++this.fEventsBeingProcessed; |
| } |
| |
| if(this.fState == STATE_ENABLED) { |
| switch(event.getType()) { |
| case IResourceChangeEvent.PRE_CLOSE: |
| case IResourceChangeEvent.PRE_DELETE:{ |
| //pre-close or pre-delete pause the persister job so it does not interfere |
| AbstractIndexManager.this.fResourceEventProcessingJob.pause(); |
| break; |
| } |
| case IResourceChangeEvent.POST_BUILD: |
| case IResourceChangeEvent.POST_CHANGE: { |
| //post change start up the indexer job and process the delta |
| AbstractIndexManager.this.fResourceEventProcessingJob.unPause(); |
| |
| // only analyze the full (starting at root) delta hierarchy |
| IResourceDelta delta = event.getDelta(); |
| if (delta != null && delta.getFullPath().toString().equals("/")) { //$NON-NLS-1$ |
| try { |
| //use visitor to visit all children |
| ResourceDeltaVisitor visitor = new ResourceDeltaVisitor( |
| AbstractIndexManager.SOURCE_RESOURCE_CHANGE); |
| delta.accept(visitor, false); |
| |
| //process any remaining batched up resources to index |
| visitor.processBatchedResourceEvents(); |
| } catch (CoreException e) { |
| Logger.logException(AbstractIndexManager.this.fName + |
| ": Failed visiting resrouce change delta. " + //$NON-NLS-1$ |
| AbstractIndexManager.LOG_ERROR_INDEX_INVALID, e); |
| } |
| } |
| break; |
| } |
| } |
| } else { |
| Logger.log(Logger.ERROR, "A resource change event came in after " + //$NON-NLS-1$ |
| AbstractIndexManager.this.fName + " shut down. This should never " + //$NON-NLS-1$ |
| "ever happen, but if it does the index may now be inconsistant."); //$NON-NLS-1$ |
| } |
| } finally { |
| //no matter how we exit be sure to update the number of events being processed |
| synchronized (this.fEventsBeingProcessedLock) { |
| --this.fEventsBeingProcessed; |
| |
| //if currently not events being processed, then notify |
| if(this.fEventsBeingProcessed == 0) { |
| this.fEventsBeingProcessedLock.notifyAll(); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * <p>Used to visit {@link IResourceDelta}s from both the {@link IResourceChangeListener} |
| * and from a {@link ISaveParticipant} given to {@link AbstractIndexManager#start(IResourceDelta, IProgressMonitor)}. |
| * The resource events are batched into groups of {@link AbstractIndexManager#BATCH_UP_AMOUNT} |
| * before being passed onto the {@link ResourceEventProcessingJob}.</p> |
| * |
| * <p><b>NOTE 1: </b> This class is intended for one time use, thus a new instance should |
| * be instantiated each time this visitor is needed to process a new {@link IResourceDelta}.</p> |
| * |
| * <p><b>NOTE 2: </b> Be sure to call {@link ResourceDeltaVisitor#processBatchedResourceEvents()} |
| * after using this visitor to be sure any remaining events get passed onto the |
| * {@link ResourceEventProcessingJob}.</p> |
| * |
| * @see ResourceDeltaVisitor#processBatchedResourceEvents() |
| */ |
| private class ResourceDeltaVisitor implements IResourceDeltaVisitor { |
| /** {@link IProgressMonitor} used to report status */ |
| private SubMonitor fProgress; |
| |
| /** |
| * <p>The source that should be used when sending resource events to the |
| * {@link ResourceEventProcessingJob}.</p> |
| * |
| * @see AbstractIndexManager#SOURCE_SAVED_STATE |
| * @see AbstractIndexManager#SOURCE_RESOURCE_CHANGE |
| */ |
| private byte fSource; |
| |
| /** |
| * <p>Due to the nature of a visitor it has no way of knowing the total amount |
| * of work it has to do but it can start to predict it based on the number of |
| * children of each event it processes and whether it plans on visiting those |
| * children</p> |
| */ |
| private int fPredictedWorkRemaining; |
| |
| /** |
| * {@link Map}<{@link IResource}, {@link ResourceEvent}> |
| * <p>Map of resources events created and batched up by this visitor. |
| * These events are periodical be sent off to the |
| * {@link ResourceEventProcessingJob} but need to be sent off |
| * one final time after this visitor finishes it work.</p> |
| * |
| * @see #processBatchedResourceEvents() |
| */ |
| private Map fBatchedResourceEvents; |
| |
| /** |
| * <p>Creates a visitor that will create resource events based on the resources |
| * it visits and using the given source as the source of the events.</p> |
| * |
| * @param source The source of the events that should be used when creating |
| * resource events from visited resources |
| * |
| * @see AbstractIndexManager#SOURCE_RESOURCE_CHANGE |
| * @see AbstractIndexManager#SOURCE_SAVED_STATE |
| */ |
| protected ResourceDeltaVisitor(byte source) { |
| this(SubMonitor.convert(null), source); |
| } |
| |
| /** |
| * <p>Creates a visitor that will create resource events based on the resources |
| * it visits and using the given source as the source of the events and |
| * report its status to the given progress as best it can as it visits |
| * resources.</p> |
| * |
| * <p><b>NOTE:</b> While the {@link SubMonitor} is provided to report status the |
| * visitor will not honor any cancellation requests.</p> |
| * |
| * @param progress Used to report status. This visitor can <b>not</b> be |
| * canceled |
| * @param source The source of the events that should be used when creating |
| * resource events from visited resources |
| * |
| * @see AbstractIndexManager#SOURCE_RESOURCE_CHANGE |
| * @see AbstractIndexManager#SOURCE_SAVED_STATE |
| */ |
| protected ResourceDeltaVisitor(SubMonitor progress, byte source) { |
| this.fProgress = progress; |
| this.fSource = source; |
| this.fBatchedResourceEvents = new LinkedHashMap(BATCH_UP_AMOUNT); |
| this.fPredictedWorkRemaining = 1; |
| } |
| |
| /** |
| * <p>Transforms each {@link IResourceDelta} into a {@link ResourceEvent}. |
| * Batches up these {@link ResourceEvent}s and then passes them onto the |
| * {@link ResourceEventProcessingJob}.</p> |
| * |
| * <p><b>NOTE 1: </b> Be sure to call {@link ResourceDeltaVisitor#processBatchedResourceEvents()} |
| * after using this visitor to be sure any remaining events get passed onto the |
| * {@link ResourceEventProcessingJob}.</p> |
| * |
| * @see org.eclipse.core.resources.IResourceDeltaVisitor#visit(org.eclipse.core.resources.IResourceDelta) |
| * @see #processBatchedResourceEvents() |
| */ |
| public boolean visit(IResourceDelta delta) throws CoreException { |
| //report status |
| this.fProgress.subTask( |
| NLS.bind(AbstractIndexManager.this.fMessage_ProcessingFiles, |
| "" + fPredictedWorkRemaining, delta.getFullPath().toString())); //$NON-NLS-1$ |
| |
| //process delta if resource not hidden |
| boolean visitChildren = false; |
| |
| /* if root node always visit its children |
| * else ask manager implementation if resource and its children should be visited |
| */ |
| if(delta.getFullPath().toString().equals("/")) { //$NON-NLS-1$ |
| visitChildren = true; |
| } else { |
| IResource resource = delta.getResource(); |
| |
| //check if should index resource or its children |
| if(isResourceToIndex(resource.getType(), resource.getFullPath())) { |
| if(resource.getType() == IResource.FILE) { |
| |
| switch (delta.getKind()) { |
| case IResourceDelta.CHANGED : { |
| /* ignore any change that is not a CONTENT, REPLACED, TYPE, |
| * or MOVE_FROM change |
| */ |
| if( !((delta.getFlags() & IResourceDelta.CONTENT) != 0) && |
| !((delta.getFlags() & IResourceDelta.REPLACED) != 0) && |
| !((delta.getFlags() & IResourceDelta.TYPE) != 0) && |
| !(((delta.getFlags() & IResourceDelta.MOVED_FROM) != 0))) { |
| |
| break; |
| } |
| } |
| //$FALL-THROUGH$ it is intended that sometimes a change will fall through to add |
| case IResourceDelta.ADDED : { |
| if((delta.getFlags() & IResourceDelta.MOVED_FROM) != 0) { |
| //create add move from action |
| this.fBatchedResourceEvents.put(resource, new ResourceEvent( |
| this.fSource, |
| AbstractIndexManager.ACTION_ADD_MOVE_FROM, |
| delta.getMovedFromPath())); |
| |
| } else { |
| //create add action |
| this.fBatchedResourceEvents.put(resource, new ResourceEvent( |
| this.fSource, |
| AbstractIndexManager.ACTION_ADD, |
| null)); |
| } |
| |
| break; |
| } |
| case IResourceDelta.REMOVED : { |
| if((delta.getFlags() & IResourceDelta.MOVED_TO) != 0) { |
| //create remove move to action |
| this.fBatchedResourceEvents.put(resource, new ResourceEvent( |
| this.fSource, |
| AbstractIndexManager.ACTION_REMOVE_MOVE_TO, |
| delta.getMovedToPath())); |
| } else { |
| //create remove action |
| this.fBatchedResourceEvents.put(resource, new ResourceEvent( |
| this.fSource, |
| AbstractIndexManager.ACTION_REMOVE, |
| null)); |
| } |
| break; |
| } |
| } |
| }//end is file |
| |
| visitChildren = true; |
| } else { |
| visitChildren = false; |
| } |
| |
| |
| |
| //deal with trying to report progress |
| if(visitChildren) { |
| this.fPredictedWorkRemaining += delta.getAffectedChildren().length; |
| } |
| this.fProgress.setWorkRemaining(this.fPredictedWorkRemaining); |
| this.fProgress.worked(1); |
| --this.fPredictedWorkRemaining; |
| |
| //batch up resource changes before sending them out |
| if(this.fBatchedResourceEvents.size() >= BATCH_UP_AMOUNT) { |
| this.processBatchedResourceEvents(); |
| } |
| } |
| |
| return visitChildren; |
| } |
| |
| /** |
| * <p>Sends any batched up resource events created by this visitor to the |
| * {@link ResourceEventProcessingJob}.<p> |
| * |
| * <p><b>NOTE:</b> This will be called every so often as the visitor is |
| * visiting resources but needs to be called a final time by the user of |
| * this visitor to be sure the final events are sent off</p> |
| */ |
| protected void processBatchedResourceEvents() { |
| AbstractIndexManager.this.fResourceEventProcessingJob.addResourceEvents( |
| this.fBatchedResourceEvents); |
| this.fBatchedResourceEvents.clear(); |
| } |
| } |
| |
| /** |
| * <p>Collects {@link ResourceEvent}s from the different sources and then processes |
| * each one by calling {@link AbstractIndexManager#performAction(byte, byte, IResource, IPath)} |
| * for each {@link ResourceEvent}.</p> |
| * |
| * @see AbstractIndexManager#performAction(byte, byte, IResource, IPath) |
| */ |
| private class ResourceEventProcessingJob extends Job { |
| /** Length to delay when scheduling job */ |
| private static final int DELAY = 500; |
| |
| /** |
| * <p>Name of the file where resource events still to index |
| * will be preserved for the next start up.</p> |
| */ |
| private static final String PRESERVED_RESOURCE_EVENTS_TO_PROCESS_FILE_NAME = ".preservedResourceEvents"; //$NON-NLS-1$ |
| |
| /** |
| * <p>This needs to be updated if {@link #preserveReceivedResourceEvents()} ever |
| * changes how it persists resource events so that {@link #loadPreservedReceivedResourceEvents(SubMonitor)} |
| * knows when opening a file that the format is of the current version and if not |
| * knows it does not know how to read the older version.</p> |
| * |
| * @see #preserveReceivedResourceEvents() |
| * @see #loadPreservedReceivedResourceEvents(SubMonitor) |
| */ |
| private static final long serialVersionUID = 1L; |
| |
| /** Whether this job has been paused or not */ |
| private volatile boolean fIsPaused; |
| |
| /** |
| * {@link Map}<{@link IResource}, {@link ResourceEvent}> |
| * <p>The list of resources events to be processed</p> |
| */ |
| private Map fResourceEvents; |
| |
| /** Lock used when accessing {@link #fBatchedResourceEvents} */ |
| private final Object fResourceEventsLock = new Object(); |
| |
| /** |
| * Locked used for allowing other jobs to wait on this job. This job |
| * will notify those waiting on this lock whenever it is done processing |
| * all resource events it currently knows about. |
| * |
| * @see #waitForConsistant(int) |
| */ |
| private final Object fToNotifyLock = new Object(); |
| |
| /** |
| * <p>Sets up this job as a long running system job</p> |
| */ |
| protected ResourceEventProcessingJob() { |
| super(AbstractIndexManager.this.fMessage_Running); |
| |
| //set this up as a long running system job |
| this.setUser(false); |
| this.setSystem(true); |
| this.setPriority(Job.LONG); |
| |
| this.fIsPaused = false; |
| this.fResourceEvents = new LinkedHashMap(); |
| } |
| |
| /** |
| * <p>Loads any preserved {@link ResourceEvent}s from the last time {@link #stop(boolean)} |
| * was invoked and schedules the job to be run</p> |
| * |
| * <p><b>NOTE: </b>Should be used instead of of calling {@link Job#schedule()} |
| * because this method also takes care of loading preserved state.</p> |
| * |
| * @param loadPreservedResourceEvents <code>true</code> if should load any |
| * preserved {@link ResourceEvent}s from the last time {@link #stop(boolean)} |
| * was invoked |
| * |
| * @return <code>true</code> if either <code>loadPreservedResourceEvents</code> |
| * was false or there was success in loading the preserved {@link ResourceEvent}s. |
| * If <code>false</code> then some {@link ResourceEvent}s may have been loosed |
| * and to insure index consistency with the workspace a full workspace re-index |
| * is needed. |
| * |
| * @see #stop(boolean) |
| */ |
| protected synchronized boolean start(boolean loadPreservedResourceEvents, |
| SubMonitor progress) { |
| |
| boolean successLoadingPreserved = true; |
| |
| //attempt to load preserved resource events if requested |
| if(!loadPreservedResourceEvents) { |
| File preservedResourceEventsFile = this.getPreservedResourceEventsFile(); |
| preservedResourceEventsFile.delete(); |
| } else { |
| successLoadingPreserved = this.loadPreservedReceivedResourceEvents(progress); |
| } |
| |
| //start up the job |
| this.schedule(); |
| |
| return successLoadingPreserved; |
| } |
| |
| /** |
| * <p>Immediately stops the job and preserves any {@link ResourceEvent}s in the queue |
| * to be processed by not yet processed if requested</p> |
| * |
| * @param preserveResourceEvents <code>true</code> to preserve any {@link ResourceEvent}s |
| * in the queue yet to be processed, <code>false</code> otherwise |
| * |
| * @return <code>true</code> if either <code>preserveResourceEvents</code> is |
| * <code>false</code> or if there was success in preserving the {@link ResourceEvent}s |
| * yet to be processed. If <code>false</code> then the preserving failed and a |
| * full workspace re-processing is needed the next time the manager is started |
| * |
| * @throws InterruptedException This could happen when trying to cancel or join |
| * the job in progress, but it really shouldn't |
| * |
| * @see #start(boolean, SubMonitor) |
| */ |
| protected synchronized boolean stop(boolean preserveResourceEvents) throws InterruptedException { |
| //this will not block indefinitely because it is known this job can be canceled |
| this.cancel(); |
| this.join(); |
| |
| //preserve if requested, else be sure no preserve file is left over for next start |
| boolean success = true; |
| if(preserveResourceEvents && this.hasResourceEventsToProcess()) { |
| success = this.preserveReceivedResourceEvents(); |
| } else { |
| this.getPreservedResourceEventsFile().delete(); |
| } |
| |
| return success; |
| } |
| |
| /** |
| * @return <code>true</code> if job is currently running or paused |
| * |
| * @see #pause() |
| * @see #unPause() |
| */ |
| protected synchronized boolean isProcessing() { |
| return this.getState() != Job.NONE || this.fIsPaused; |
| } |
| |
| /** |
| * <p>Un-pauses this job. This has no effect if the job is already running.</p> |
| * <p>This should be used in place of {@link Job#schedule()} to reset state |
| * caused by calling {@link #pause()}</p> |
| * |
| * @see #pause() |
| */ |
| protected synchronized void unPause() { |
| this.fIsPaused = false; |
| |
| //get the job running again depending on its current state |
| if(this.getState() == Job.SLEEPING) { |
| this.wakeUp(DELAY); |
| } else { |
| this.schedule(DELAY); |
| } |
| } |
| |
| /** |
| * <p>Pauses this job, even if it is running</p> |
| * <p>This should be used in place of {@link Job#sleep()} because {@link Job#sleep()} |
| * will not pause a job that is already running but calling this will pause this job |
| * even if it is running. {@link #unPause()} must be used to start this job again</p> |
| * |
| * @see #unPause() |
| */ |
| protected synchronized void pause() { |
| //if job is already running this will force it to pause |
| this.fIsPaused = true; |
| |
| //this only works if the job is not running |
| this.sleep(); |
| } |
| |
| /** |
| * <p>Adds a batch of {@link ResourceEvent}s to the queue of events to be processed. |
| * Will also un-pause the job if it is not already running</p> |
| * |
| * @param resourceEvents {@link Map}<{@link IResource}, {@link ResourceEvent}> |
| * A batch of {@link ResourceEvent}s to be processed |
| * |
| * @see #addResourceEvent(ResourceEvent) |
| * @see #unPause() |
| */ |
| protected void addResourceEvents(Map resourceEvents) { |
| Iterator iter = resourceEvents.keySet().iterator(); |
| while(iter.hasNext()) { |
| IResource resource = (IResource)iter.next(); |
| ResourceEvent resourceEvent = (ResourceEvent)resourceEvents.get(resource); |
| addResourceEvent(resource, resourceEvent); |
| } |
| |
| //un-pause the processor if it is not already running |
| if(!isProcessing()) { |
| this.unPause(); |
| } |
| } |
| |
| /** |
| * <p>Gets the number of {@link ResourceEvent}s left to process by this job. This |
| * count is only valid for the exact moment it is returned because events are |
| * constantly being added and removed from the queue of events to process</p> |
| * |
| * @return the number of {@link ResourceEvent}s left to process |
| */ |
| protected int getNumResourceEventsToProcess() { |
| return this.fResourceEvents.size(); |
| } |
| |
| /** |
| * <p>Blocks until either the given timeout elapses (0 means never to timeout), or |
| * there are currently no {@link ResourceEvent}s to process or being processed |
| * by this job</p> |
| * |
| * @param timeout block until either this timeout elapses (0 means never to timeout) |
| * or there are currently no {@link ResourceEvent}s to process or being processed |
| * by this job |
| * |
| * @throws InterruptedException This can happen when waiting for a lock |
| */ |
| protected void waitForConsistant(int timeout) throws InterruptedException { |
| if(hasResourceEventsToProcess() || isProcessing()) { |
| synchronized (this.fToNotifyLock) { |
| this.fToNotifyLock.wait(timeout); |
| } |
| } |
| } |
| |
| /** |
| * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor) |
| */ |
| protected IStatus run(IProgressMonitor monitor) { |
| try { |
| //report status |
| SubMonitor progress = SubMonitor.convert(monitor); |
| |
| while(!this.fIsPaused && !monitor.isCanceled() && this.hasResourceEventsToProcess()) { |
| //report status |
| progress.setTaskName(NLS.bind(AbstractIndexManager.this.fMessage_ProcessingFiles, |
| "" + getNumResourceEventsToProcess())); //$NON-NLS-1$ |
| progress.setWorkRemaining(getNumResourceEventsToProcess()); |
| |
| //get the next event to process |
| ResourceEvent resourceEvent = null; |
| IResource resource = null; |
| synchronized (this.fResourceEventsLock) { |
| resource = (IResource) this.fResourceEvents.keySet().iterator().next(); |
| resourceEvent = (ResourceEvent)this.fResourceEvents.remove(resource); |
| } |
| |
| //report status |
| monitor.subTask(resource.getName()); |
| |
| //perform action safely |
| final byte source = resourceEvent.fSource; |
| final byte action = resourceEvent.fAction; |
| final IResource finResource = resource; |
| final IPath movePath = resourceEvent.fMovePath; |
| SafeRunner.run(new ISafeRunnable() { |
| public void run() throws Exception { |
| AbstractIndexManager.this.performAction(source, action, |
| finResource, movePath); |
| } |
| |
| public void handleException(Throwable e) { |
| Logger.logException("Error while performing an update to the index. " + //$NON-NLS-1$ |
| AbstractIndexManager.LOG_ERROR_INDEX_INVALID, e); |
| } |
| }); |
| |
| //report progress |
| progress.worked(1); |
| |
| //avoid dead locks |
| Job.getJobManager().currentJob().yieldRule(monitor); |
| } |
| |
| //done work |
| monitor.done(); |
| } finally { |
| //want to be sure we notify no matter how we exit |
| this.notifyIfConsistant(); |
| } |
| |
| /* if canceled then return CANCEL, |
| * else if done or paused return OK |
| */ |
| IStatus exitStatus; |
| if(monitor.isCanceled()) { |
| exitStatus = Status.CANCEL_STATUS; |
| } else { |
| exitStatus = Status.OK_STATUS; |
| } |
| |
| return exitStatus; |
| } |
| |
| /** |
| * <p>If resource not already scheduled to be processed, schedule it |
| * else if resource already scheduled to be processed, update the action only if |
| * the new action comes from a resource change event.</p> |
| * |
| * <p>Ignore other sources for updating existing resource events because all other |
| * sources are "start-up" type sources and thus only {@link ResourceEvent} with a |
| * source of a resource change event trump existing events.</p> |
| * |
| * @param resourceEvent {@link ResourceEvent} to be processed by this job |
| */ |
| private void addResourceEvent(IResource resource, ResourceEvent resourceEvent) { |
| |
| synchronized (this.fResourceEventsLock) { |
| /* if resource not already scheduled to be processed, schedule it |
| * else if resource already scheduled to be processed, update the action only if |
| * the new action comes from a resource change event |
| */ |
| if(!this.fResourceEvents.containsKey(resource)) { |
| this.fResourceEvents.put(resource, resourceEvent); |
| } else if(resourceEvent.fSource == AbstractIndexManager.SOURCE_RESOURCE_CHANGE) { |
| ((ResourceEvent)this.fResourceEvents.get(resource)).fAction = resourceEvent.fAction; |
| } else { |
| //Purposely ignoring all other resource events |
| } |
| } |
| } |
| |
| /** |
| * @return <code>true</code> if there are any resources to process, |
| * <code>false</code> otherwise |
| */ |
| private boolean hasResourceEventsToProcess() { |
| return !this.fResourceEvents.isEmpty(); |
| } |
| |
| /** |
| * <p>Preserves all of the resource events that have been received by this |
| * manager but not yet processed</p> |
| * |
| * <p>If this operation was successful then the next time the manager starts |
| * it can load these events and process them. If it was not successful then a |
| * full re-processing of the entire workspace will need to take place to be |
| * sure the index is consistent.</p> |
| * |
| * <p><b>NOTE:</b> If this method changes how it preserves these events then |
| * {@link #serialVersionUID} will need to be incremented so that the manager |
| * does not attempt to load an old version of the file that may exist in a users |
| * workspace. Also {@link #loadPreservedReceivedResourceEvents(SubMonitor)} will |
| * have to be updated to load the new file structure.</p> |
| * |
| * @return <code>true</code> if successfully preserved the resource events that |
| * have been received by not yet processed, <code>false</code> otherwise |
| * |
| * @see #serialVersionUID |
| * @see #loadPreservedReceivedResourceEvents(SubMonitor) |
| */ |
| private boolean preserveReceivedResourceEvents() { |
| File preservedResourceEventsFile = this.getPreservedResourceEventsFile(); |
| boolean success = true; |
| |
| synchronized (this.fResourceEventsLock) { |
| DataOutputStream dos = null; |
| try { |
| //if file already exists delete it |
| if(preservedResourceEventsFile.exists()) { |
| preservedResourceEventsFile.delete(); |
| preservedResourceEventsFile.createNewFile(); |
| } |
| |
| //create output objects |
| FileOutputStream fos = new FileOutputStream(preservedResourceEventsFile); |
| BufferedOutputStream bos = new BufferedOutputStream(fos); |
| dos = new DataOutputStream(bos); |
| |
| //write serial version |
| dos.writeLong(serialVersionUID); |
| |
| //write size |
| dos.writeInt(this.getNumResourceEventsToProcess()); |
| |
| //write out all the information needed to restore the resource events to process |
| Iterator iter = this.fResourceEvents.keySet().iterator(); |
| while(iter.hasNext()) { |
| IResource resource = (IResource)iter.next(); |
| ResourceEvent resourceEvent = (ResourceEvent)this.fResourceEvents.get(resource); |
| |
| if(resourceEvent.fSource != AbstractIndexManager.SOURCE_WORKSPACE_SCAN) { |
| //write out information |
| dos.writeByte(resourceEvent.fAction); |
| dos.writeByte(resource.getType()); |
| byte[] pathBytes = resource.getFullPath().toString().getBytes(ENCODING_UTF16); |
| dos.write(pathBytes); |
| dos.writeByte('\0'); |
| if(resourceEvent.fMovePath != null) { |
| dos.writeBytes(resourceEvent.fMovePath.toPortableString()); |
| } |
| dos.writeByte('\0'); |
| } |
| } |
| |
| this.fResourceEvents.clear(); |
| |
| dos.flush(); |
| } catch (FileNotFoundException e) { |
| Logger.logException(AbstractIndexManager.this.fName + |
| ": Exception while opening file to preserve resrouces to index.", //$NON-NLS-1$ |
| e); |
| success = false; |
| } catch (IOException e) { |
| Logger.logException(AbstractIndexManager.this.fName + |
| ": Exception while writing to file to preserve resrouces to index.", //$NON-NLS-1$ |
| e); |
| success = false; |
| } finally { |
| //be sure to close output |
| if(dos != null) { |
| try { |
| dos.close(); |
| } catch (IOException e) { |
| Logger.logException(AbstractIndexManager.this.fName + |
| ": Exception while closing file with preserved resources to index.", //$NON-NLS-1$ |
| e); |
| success = false; |
| } |
| } |
| } |
| |
| //if failed, for consistency must do a full re-process next workspace load |
| if(!success) { |
| preservedResourceEventsFile.delete(); |
| } |
| } |
| |
| return success; |
| } |
| |
| /** |
| * <p>Loads the received resource events that were preserved during the manager's |
| * last shut down so they can be processed now</p> |
| * |
| * <p>If this operation is not successful then a full re-processing of the |
| * entire workspace is needed to be sure the index is consistent.</p> |
| * |
| * @param progress used to report status of loading the preserved received resource events |
| * @return <code>true</code> if the loading of the preserved received resource events |
| * was successful, <code>false</code> otherwise. |
| * |
| * @see #serialVersionUID |
| * @see #preserveReceivedResourceEvents() |
| */ |
| private boolean loadPreservedReceivedResourceEvents(SubMonitor progress) { |
| progress.subTask(AbstractIndexManager.this.fMessage_Initializing); |
| |
| boolean success = true; |
| File preservedResourceEventsFile = this.getPreservedResourceEventsFile(); |
| |
| if(preservedResourceEventsFile.exists()) { |
| Map preservedResourceEvents = null; |
| |
| DataInputStream dis = null; |
| try { |
| FileInputStream fis = new FileInputStream(preservedResourceEventsFile); |
| BufferedInputStream bis = new BufferedInputStream(fis); |
| dis = new DataInputStream(bis); |
| |
| //check serial version first |
| long preservedSerialVersionUID = dis.readLong(); |
| if(preservedSerialVersionUID == serialVersionUID) { |
| |
| //read each record |
| int numberOfRecords = dis.readInt(); |
| preservedResourceEvents = new LinkedHashMap(numberOfRecords); |
| progress.setWorkRemaining(numberOfRecords); |
| for(int i = 0; i < numberOfRecords; ++i) { |
| //action is first byte |
| byte action = dis.readByte(); |
| |
| //file type is the next byte |
| byte fileType = dis.readByte(); |
| |
| //resource location are the next bytes |
| ByteArrayOutputStream resourceLocoationBOS = new ByteArrayOutputStream(); |
| byte b = dis.readByte(); |
| while(b != '\0') { |
| resourceLocoationBOS.write(b); |
| b = dis.readByte(); |
| } |
| |
| //move path are the next bytes |
| ByteArrayOutputStream movePathBOS = new ByteArrayOutputStream(); |
| b = dis.readByte(); |
| while(b != '\0') { |
| movePathBOS.write(b); |
| b = dis.readByte(); |
| } |
| |
| //get the resource |
| IResource resource = null; |
| IPath resourcePath = new Path(new String(resourceLocoationBOS.toByteArray(), ENCODING_UTF16)); |
| if(fileType == IResource.FILE) { |
| resource = |
| ResourcesPlugin.getWorkspace().getRoot().getFile(resourcePath); |
| } else { |
| resource = |
| ResourcesPlugin.getWorkspace().getRoot().getFolder(resourcePath); |
| } |
| |
| //get the move path |
| IPath movePath = null; |
| if(movePathBOS.size() != 0) { |
| movePath = new Path(new String(movePathBOS.toByteArray(), ENCODING_UTF16)); |
| } |
| |
| //add the object to the list of of preserved resources |
| preservedResourceEvents.put(resource, new ResourceEvent( |
| AbstractIndexManager.SOURCE_PRESERVED_RESOURCES_TO_INDEX, |
| action, movePath)); |
| |
| progress.worked(1); |
| } |
| } else { |
| success = false; |
| } |
| } catch (FileNotFoundException e) { |
| Logger.logException(AbstractIndexManager.this.fName + |
| ": Exception while opening file to read preserved resources to index.", //$NON-NLS-1$ |
| e); |
| success = false; |
| } catch (IOException e) { |
| Logger.logException(AbstractIndexManager.this.fName + |
| ": Exception while reading from file of preserved resources to index.", //$NON-NLS-1$ |
| e); |
| success = false; |
| } finally { |
| if(dis != null) { |
| try { |
| dis.close(); |
| } catch (IOException e) { |
| Logger.logException(AbstractIndexManager.this.fName + |
| ": Exception while closing file of preserved resources" + //$NON-NLS-1$ |
| " to index that was just read. This should have no" + //$NON-NLS-1$ |
| " effect on the consistency of the index.", //$NON-NLS-1$ |
| e); |
| } |
| } |
| } |
| |
| //if success loading preserved then add to master list |
| if(success) { |
| synchronized (this.fResourceEventsLock) { |
| Iterator iter = preservedResourceEvents.keySet().iterator(); |
| while(iter.hasNext()) { |
| IResource resource = (IResource)iter.next(); |
| ResourceEvent event = (ResourceEvent)preservedResourceEvents.get(resource); |
| this.fResourceEvents.put(resource, event); |
| } |
| } |
| } else { |
| //failed reading file, so delete it |
| preservedResourceEventsFile.delete(); |
| } |
| } |
| |
| return success; |
| } |
| |
| /** |
| * @return {@link File} that contains any resource events received but not processed |
| * by this manager the last time it shutdown. This file may or may not actually |
| * exist. |
| * |
| * @see #preserveReceivedResourceEvents() |
| * @see #loadPreservedReceivedResourceEvents(SubMonitor) |
| */ |
| private File getPreservedResourceEventsFile() { |
| IPath preservedResroucesToIndexPath = |
| AbstractIndexManager.this.getWorkingLocation().append( |
| PRESERVED_RESOURCE_EVENTS_TO_PROCESS_FILE_NAME); |
| return new File(preservedResroucesToIndexPath.toOSString()); |
| } |
| |
| /** |
| * <p>If all resource events have been processed |
| */ |
| private void notifyIfConsistant() { |
| if(!this.hasResourceEventsToProcess()) { |
| synchronized (this.fToNotifyLock) { |
| this.fToNotifyLock.notifyAll(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * <p>Represents a resource that was discovered by this manager. Contains |
| * all the information this manager and the index needs to know about this |
| * resource. Such has how the manager was notified about this resource and |
| * the type of action occurring on the resource.</p> |
| */ |
| private static class ResourceEvent { |
| /** |
| * <p>The source of this resource event</p> |
| * |
| * @see AbstractIndexManager#SOURCE_RESOURCE_CHANGE |
| * @see AbstractIndexManager#SOURCE_SAVED_STATE |
| * @see AbstractIndexManager#SOURCE_WORKSPACE_SCAN |
| * @see AbstractIndexManager#SOURCE_PRESERVED_RESOURCES_TO_INDEX |
| */ |
| protected byte fSource; |
| |
| /** |
| * <p>The action that the index should take with this resource</p> |
| * |
| * @see AbstractIndexManager#ACTION_ADD |
| * @see AbstractIndexManager#ACTION_REMOVE |
| * @see AbstractIndexManager#ACTION_ADD_MOVE_FROM |
| * @see AbstractIndexManager#ACTION_REMOVE_MOVE_TO |
| */ |
| protected byte fAction; |
| |
| /** |
| * |
| * <p>If the {@link #fAction} is {@link AbstractIndexManager#ACTION_ADD_MOVE_FROM} or |
| * {@link AbstractIndexManager#ACTION_REMOVE_MOVE_TO} then this field will have the |
| * path the resource was moved from or moved to. Else this field will be <code>null</code></p> |
| * |
| * <p><b>NOTE: </b>Maybe <code>null</code>.</p> |
| * |
| * @see AbstractIndexManager#ACTION_ADD_MOVE_FROM |
| * @see AbstractIndexManager#ACTION_REMOVE_MOVE_TO |
| */ |
| protected IPath fMovePath; |
| |
| /** |
| * <p>Creates a resource event that the index needs to react to in some way</p> |
| * |
| * @param source source that the manager used to learn of this resource |
| * @param action action the index should take on this resource |
| * @param resource resource that the index should know about |
| * @param movePath if action is {@link AbstractIndexManager#ACTION_ADD_MOVE_FROM} or |
| * {@link AbstractIndexManager#ACTION_REMOVE_MOVE_TO} then this should be the path the |
| * resource was moved from or moved to respectively, else should be <code>null</code> |
| * |
| * @see AbstractIndexManager#SOURCE_RESOURCE_CHANGE |
| * @see AbstractIndexManager#SOURCE_SAVED_STATE |
| * @see AbstractIndexManager#SOURCE_WORKSPACE_SCAN |
| * @see AbstractIndexManager#SOURCE_PRESERVED_RESOURCES_TO_INDEX |
| * |
| * @see AbstractIndexManager#ACTION_ADD |
| * @see AbstractIndexManager#ACTION_REMOVE |
| * @see AbstractIndexManager#ACTION_ADD_MOVE_FROM |
| * @see AbstractIndexManager#ACTION_REMOVE_MOVE_TO |
| */ |
| protected ResourceEvent(byte source, byte action, IPath movePath) { |
| this.fSource = source; |
| this.fAction = action; |
| this.fMovePath = movePath; |
| } |
| } |
| } |