/*******************************************************************************
 * Copyright (c) 2010, 2021 IBM Corporation and others.
 * All rights reserved. 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.wst.sse.core.indexing;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
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 java.util.Map.Entry;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
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.Platform;
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;
import org.eclipse.wst.sse.core.internal.SSECoreMessages;

/**
 * <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>
 * 
 */
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;

	/** {@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;

	private static final boolean DEBUG = Platform.getDebugBoolean("org.eclipse.wst.sse.core/index");
	private static final boolean DEBUG_LIFECYCLE = Platform.getDebugBoolean("org.eclipse.wst.sse.core/index/lifecycle");

	/**
	 * <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
	 */
	protected AbstractIndexManager(String name) {
		this.fName = name;
		this.fState = STATE_DISABLED;
		this.fResourceChangeListener = new ResourceChangeListener();
		this.fResourceEventProcessingJob = new ResourceEventProcessingJob();
		this.fStarting = false;
	}

	/**
	 * <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
	 *            ignored
	 * @param messageInitializing
	 *            ignored
	 * @param messageProcessingFiles
	 *            ignored
	 * 
	 * @deprecated This constructor ignores the last three parameters.
	 * @see #AbstractIndexManager(String)
	 */

	protected AbstractIndexManager(String name, String messageRunning, String messageInitializing, String messageProcessingFiles) {
		this(name);
	}

	/**
	 * <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;

			try {
				if (this.fState == STATE_DISABLED) {
					// report status
					progress.beginTask(NLS.bind(SSECoreMessages.IndexManager_0_starting, this.fName), 2);

					// start listening for resource change events
					this.fResourceChangeListener.start();

					// check to see if a full re-index is required
					boolean fullReindexNeeded = isFullReIndexNeeded();

					/*
					 * start the indexing job only loading preserved state, if
					 * not doing full index. if failed loading preserved state,
					 * then force full re-index
					 */
					fullReindexNeeded |= !this.fResourceEventProcessingJob.start(!fullReindexNeeded, progress.newChild(1));
					progress.setWorkRemaining(1);

					// don't bother processing saved delta if forced full
					// re-index is needed
					if (!fullReindexNeeded) {
						/*
						 * if there is a delta attempt to process it else need
						 * to do a full workspace index
						 */
						if (savedStateDelta != null) {
							try {
								// deal with reporting progress
								SubMonitor savedStateProgress = progress.newChild(1, SubMonitor.SUPPRESS_NONE);

								savedStateProgress.setTaskName(NLS.bind(SSECoreMessages.IndexManager_0_starting_1, new String[]{this.fName, SSECoreMessages.IndexManager_processing_deferred_resource_changes}));

								// 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) {
								fullReindexNeeded = 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 {
							fullReindexNeeded = true;
						}
					}
					else {
						if (DEBUG_LIFECYCLE && DEBUG) {
							Logger.log(Logger.INFO_DEBUG, "Fully reindexing for " + getClass().getName());
						}
					}
					progress.worked(1);

					/* If need to process the entire workspace do so in
					   another job */
					if (fullReindexNeeded) {
						this.fWorkspaceVisitorJob = new ResourceVisitorJob();
						this.fWorkspaceVisitorJob.schedule();
					}

					// update state
					this.fState = STATE_ENABLED;
				}
			}
			finally {
				this.fStarting = false;
				progress.done();
			}
		}
	}

	/**
	 * <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();
				}

				doStop();

				// update status
				this.fState = STATE_DISABLED;
			}
		}
	}

	protected void doStop() {
		// any stop logic
	}

	/**
	 * @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(NLS.bind(SSECoreMessages.IndexManager_Waiting_for_0, this.fName), remainingWork);

		// wait for start up
		if (this.fStarting && !monitor.isCanceled()) {
			SubMonitor startingProgress = progress.newChild(1);
			startingProgress.subTask(NLS.bind(SSECoreMessages.IndexManager_0_starting, this.fName));
			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(SSECoreMessages.IndexManager_Processing_entire_workspace_for_the_first_time);
			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(SSECoreMessages.IndexManager_processing_recent_resource_changes);
			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 prevNumResources;
			int numResources = this.fResourceEventProcessingJob.getNumResourceEventsToProcess();
			while (numResources != 0 && !monitor.isCanceled()) {
				// update the progress indicator
				indexingProgress.subTask(NLS.bind(SSECoreMessages.IndexManager_0_Indexing_1_Files, new Object[]{AbstractIndexManager.this.fName, "" + numResources})); //$NON-NLS-1$
				indexingProgress.setWorkRemaining(numResources);
				prevNumResources = numResources;
				numResources = this.fResourceEventProcessingJob.getNumResourceEventsToProcess();
				int numProcessed = prevNumResources - 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();
		}

		progress.done();
		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>
	 */
	protected void forceFullReIndexNextStart() {
		IPath reIndexLocation = AbstractIndexManager.this.getWorkingLocation().append(RE_PROCESS_FILE_NAME);
		File file = reIndexLocation.toFile();
		try {
			file.createNewFile();
		}
		catch (IOException e) {
			Logger.logException(this.fName + ": Could not create file to tell manager to" + " do a full re-index on next load. " + AbstractIndexManager.LOG_ERROR_INDEX_INVALID, e); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * @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 reindexLocation = AbstractIndexManager.this.getWorkingLocation().append(RE_PROCESS_FILE_NAME);
		File file = reindexLocation.toFile();
		if (file.exists()) {
			forcedFullReIndexNeeded = true;
			file.delete();
		}

		return forcedFullReIndexNeeded;
	}

	/**
	 * @return <code>true</code> if a full workspace index is needed as
	 *         dictated by this indexer, <code>false</code>
	 *         otherwise
	 *         @since 1.2.0
	 */
	protected boolean isFullReIndexNeeded() {
		return _isForcedFullReIndexNeeded();
	}
	/**
	 * <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 ResourceVisitorJob extends Job {
		IResource fTarget = null;
		/**
		 * @param target - The root whose children may be visited. If not specified,
		 *                 the workspace root will be used.
		 */
		ResourceVisitorJob(IResource target) {
			this();
			fTarget = target;
		}

		protected ResourceVisitorJob() {
			super(NLS.bind(SSECoreMessages.IndexManager_0_Processing_entire_workspace_for_the_first_time, AbstractIndexManager.this.fName));

			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(NLS.bind(SSECoreMessages.IndexManager_Indexing01, AbstractIndexManager.this.fName, (fTarget != null? fTarget.getFullPath().makeRelative().toString() : "entire workspace")), IProgressMonitor.UNKNOWN);

				// visit the workspace
				ResourceVisitor visitor = new ResourceVisitor(monitor);
				if (fTarget != null) {
					fTarget.accept(visitor, IResource.NONE);
				} else {
					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 " + (fTarget != null? fTarget.getFullPath() : "entire workspace") + " for initial index. " + AbstractIndexManager.LOG_ERROR_INDEX_INVALID, e); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			}

			IStatus status;
			if (monitor.isCanceled()) {
				status = Status.CANCEL_STATUS;
			}
			else {
				status = Status.OK_STATUS;
			}

			monitor.done();
			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 ResourceVisitor#processBatchedResourceEvents() must be
	 * called to flush out the last of the {@link ResourceEvent}s produced
	 * by this visitor.
	 * </p>
	 */
	private class ResourceVisitor implements IResourceProxyVisitor {
		/**
		 * {@link IProgressMonitor} used to report status and check for
		 * cancellation
		 */
		private SubMonitor fProgress;

		/**
		 * {@link Map}&lt{@link IResource}, {@link ResourceEvent}&gt
		 * <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<IResource, ResourceEvent> fBatchedResourceEvents;

		/**
		 * <p>
		 * Default constructor
		 * </p>
		 * 
		 * @param monitor
		 *            used to report status and allow this visitor to be
		 *            canceled
		 */
		protected ResourceVisitor(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 (proxy.isDerived()) {
					/*
					 * Do not include derived resources
					 */
					visitChildren = false;
				}
				else if (proxy.requestFullPath().isRoot()) {
					visitChildren = true;
				}
				else if (isResourceToIndex(proxy.getType(), proxy.requestFullPath())) {
					if (proxy.getType() == IResource.FILE && proxy.isAccessible()) {
						// add the file to be indexed
						IFile file = (IFile) proxy.requestResource();
						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 : {
							/*
							 * 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 resource change delta. " + AbstractIndexManager.LOG_ERROR_INDEX_INVALID, e); //$NON-NLS-1$
								}
							}
							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}&lt{@link IResource}, {@link ResourceEvent}&gt
		 * <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<IResource, ResourceEvent> 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(SSECoreMessages.IndexManager_0_resources_to_go_1, new Object[]{"" + 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.getResource().isDerived()) {
				/*
				 * Do not include ANY derived resources
				 */
				visitChildren = false;
			}
			else if (delta.getFullPath().isRoot()) { //$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;
								}
							}
							/*
							 * it is intended that sometimes a change will
							 * fall through to add
							 */
							//$FALL-THROUGH$
							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
					else if (resource.getType() == IResource.PROJECT) {
						if ((delta.getFlags() & IResourceDelta.OPEN) != 0 && ((IProject) resource).isOpen()) {
							if (DEBUG_LIFECYCLE && DEBUG) {
								Logger.log(Logger.INFO_DEBUG, "Indexing project " + resource.getName() + " for " + AbstractIndexManager.this.getName());
							}
							new ResourceVisitorJob(delta.getResource()).schedule();
						}
					}

					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 = 2L;

		/** Whether this job has been paused or not */
		private volatile boolean fIsPaused;

		/**
		 * {@link Map}&lt{@link IResource}, {@link ResourceEvent}&gt
		 * <p>
		 * The list of resources events to be processed
		 * </p>
		 */
		private Map<IResource, ResourceEvent> 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(NLS.bind(SSECoreMessages.IndexManager_0_Processing_resource_events, AbstractIndexManager.this.fName));

			// 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, <code>false</code>
		 *            to discard them
		 * 
		 * @return <code>true</code> if the preserved events were successfully loaded.
		 *         if <code>false</code> is returned, a full workspace re-index is needed.
		 * 
		 * @see #stop(boolean)
		 */
		protected synchronized boolean start(boolean loadPreservedResourceEvents, SubMonitor progress) {
			boolean successLoadingPreserved = false;

			// attempt to load preserved resource events if requested
			if (loadPreservedResourceEvents) {
				successLoadingPreserved = this.loadPreservedReceivedResourceEvents(progress);
			}
			else {
				File preservedResourceEventsFile = this.getPreservedResourceEventsFile();
				preservedResourceEventsFile.delete();
			}

			// 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}&lt{@link IResource}, {@link ResourceEvent}
		 *            &gt A batch of {@link ResourceEvent}s to be processed
		 * 
		 * @see #addResourceEvent(ResourceEvent)
		 * @see #unPause()
		 */
		protected void addResourceEvents(Map<IResource, ResourceEvent> resourceEvents) {
			Iterator<Map.Entry<IResource, ResourceEvent>> iter = resourceEvents.entrySet().iterator();
			while (iter.hasNext()) {
				Entry<IResource, ResourceEvent> entry = iter.next();
				IResource resource = entry.getKey();
				ResourceEvent resourceEvent = entry.getValue();
				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(SSECoreMessages.IndexManager_0_Indexing_1_Files, new Object[]{AbstractIndexManager.this.fName, "" + getNumResourceEventsToProcess()})); //$NON-NLS-1$
					progress.setWorkRemaining(getNumResourceEventsToProcess());

					// get the next event to process
					ResourceEvent resourceEvent = null;
					IResource resource = null;
					synchronized (this.fResourceEventsLock) {
						resource = this.fResourceEvents.keySet().iterator().next();
						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) {
					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<Map.Entry<IResource, ResourceEvent>> iter = this.fResourceEvents.entrySet().iterator();
					while (iter.hasNext()) {
						Map.Entry<IResource, ResourceEvent> entry = iter.next();
						IResource resource = entry.getKey();
						ResourceEvent resourceEvent = entry.getValue();

						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.writeInt(pathBytes.length);
							dos.write(pathBytes);
							pathBytes = resourceEvent.fMovePath != null ? resourceEvent.fMovePath.toPortableString().getBytes(ENCODING_UTF16) : new byte[0];
							dos.writeInt(pathBytes.length);
							if (pathBytes.length > 0) {
								dos.write(pathBytes);
							}
						}
					}

					this.fResourceEvents.clear();

					dos.flush();
				}
				catch (FileNotFoundException e) {
					Logger.logException(AbstractIndexManager.this.fName + ": Exception while opening file to preserve resources to index.", //$NON-NLS-1$
								e);
					success = false;
				}
				catch (IOException e) {
					Logger.logException(AbstractIndexManager.this.fName + ": Exception while writing to file to preserve resources 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(SSECoreMessages.IndexManager_processing_deferred_resource_changes);

			boolean success = true;
			File preservedResourceEventsFile = this.getPreservedResourceEventsFile();

			if (preservedResourceEventsFile.exists()) {
				Map<IResource, ResourceEvent> 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
							final String resourceLocation = readStringFromStream(dis);
							// get the resource
							IResource resource = null;
							IPath resourcePath = new Path(resourceLocation);
							if (!resourcePath.isRoot() && resourcePath.segmentCount() > 1) {
								if (fileType == IResource.FILE) {
									resource = ResourcesPlugin.getWorkspace().getRoot().getFile(resourcePath);
								}
								else {
									resource = ResourcesPlugin.getWorkspace().getRoot().getFolder(resourcePath);
								}
							}
							else {
								Logger.log(Logger.WARNING, "The AbstractIndexManager " + AbstractIndexManager.this.fName + " attempted to load an invlaid preserved resource event:\n" + "(" + resourcePath + ")");
							}

							// move path are the next bytes
							final String moveLocation = readStringFromStream(dis);
							// get the move path
							IPath movePath = null;
							if (moveLocation.length() > 0) {
								movePath = new Path(moveLocation);
							}

							// 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. Index manager will recover by re-indexing workspace.", //$NON-NLS-1$
								e);
					success = false;
				}
				catch (IOException e) {
					Logger.logException(AbstractIndexManager.this.fName + ": Exception while reading from file of preserved resources to index. Index manager will recover by re-indexing workspace.", //$NON-NLS-1$
								e);
					success = false;
				}
				catch (Exception e) {
					// Purposely catching all exceptions here so that index
					// manager can recover gracefully
					Logger.logException(AbstractIndexManager.this.fName + ": Unexpected exception while reading from file of preserved resources to index. Index manager will recover by re-indexing workspace.", //$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 && preservedResourceEvents != null) {
					synchronized (this.fResourceEventsLock) {
						Iterator<Map.Entry<IResource, ResourceEvent>> iter = preservedResourceEvents.entrySet().iterator();
						while (iter.hasNext()) {
							Entry<IResource, ResourceEvent> entry = iter.next();
							IResource resource = entry.getKey();
							ResourceEvent event = entry.getValue();
							this.fResourceEvents.put(resource, event);
						}
					}
				}
				else {
					// failed reading file, so delete it
					preservedResourceEventsFile.delete();
				}
			}

			progress.done();
			return success;
		}

		/**
		 * Reads a string from the input stream. An integer length is read first
		 * followed by the bytes of the string
		 * @param dis the input stream to read from
		 * @return a String represented by the bytes
		 * @throws IOException
		 */
		private String readStringFromStream(DataInputStream dis) throws IOException {
			// Read the int for the string's length
			final int length = dis.readInt();
			// Read in length bytes for the string
			final byte[] resourceLocation = new byte[length];
			int read = 0;
			int offset = 0;
			while (offset < resourceLocation.length && (read = dis.read(resourceLocation, offset, resourceLocation.length - offset)) > 0) {
				offset += read;
			}
			return new String(resourceLocation, ENCODING_UTF16);
		}

		/**
		 * @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 preservedResourcesToIndexLocation = AbstractIndexManager.this.getWorkingLocation().append(PRESERVED_RESOURCE_EVENTS_TO_PROCESS_FILE_NAME);
			return preservedResourcesToIndexLocation.toFile();
		}

		/**
		 * <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;
		}
	}
}