/*******************************************************************************
 * Copyright (c) 2002, 2005 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 com.example.movedeletehook;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.team.IMoveDeleteHook;
import org.eclipse.core.resources.team.IResourceTree;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

/**
 * Example implementation of <code>IMoveDeleteHook</code> illustrating
 * how this hook should be used.
 * <p>
 * A word on terminology. The code in this hook lives in a world where it
 * must deal with two parallel notions of "file" that must not be confused.
 * The terminology we use to keep these straight:
 * <ul>
 * <li><b>local file system</b> - We use "local file system" (abbreviated "lfs")
 * when talking about files and folders (directories) on disk; these are 
 * accessed through <code>java.io.File</code> protocol.
 * </li> 
 * <li><b>workspace resource tree</b> - We use "workspace resource tree"
 * when talking about in the Eclipse Platform's in-memory representation of
 * files and folders; these are ordinarily accessed through 
 * <code>org.eclipse.core.resources.IResource</code> protocol.
 * </li> 
 * </ul>
 * The general game being played is simple: The hook implementation deals
 * directly with the files in the local file system using whatever means it
 * has at its disposal, and then directs the Eclipse platform how to update the
 * workspace resource tree to match using methods on the
 * <code>org.eclipse.core.resources.team.IResourceTree</code>
 * object passed to the hook on each occasion.
 * </p>
 * <p>
 * N.B. It is important for all core methods to update the progress monitor so 
 * that the user knows that long-running operations are making progress 
 * (and can cancel if required). Progress monitoring was omitted here only
 * because it made the code more difficult to read without shedding much light
 * on how to use this hook correctly. Please do not follow our bad example.
 * </p>
 * 
 * @see org.eclipse.core.resources.IResource
 * @see org.eclipse.core.resources.team.IMoveDeleteHook
 * @see org.eclipse.core.resources.team.IResourceTree
 */
public class MoveDeleteHookExample1 implements IMoveDeleteHook {

	/**
	 * Creates a new hook instance.
	 */
	public MoveDeleteHookExample1() {
	}

	/**
	 * This <code>IMoveDeleteHook</code> method implements 
	 * <code>IResource.delete(int,IProgressMonitor)</code> where the receiver is
	 * a file. This example implementation illustrates the steps involved 
	 * (except for progress monitoring).
	 * 
	 * @param tree the workspace resource tree; this object is only valid 
	 *    for the duration of the invocation of this method, and must not be 
	 *    used after this method has completed
	 * @param file the handle of the file to delete; the receiver of
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @param updateFlags bit-wise or of update flag constants as per 
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @param monitor the progress monitor, or <code>null</code> as per 
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @return <code>false</code> if this method declined to assume 
	 *   responsibility for this operation, and <code>true</code> if this method
	 *   attempted to carry out the operation
	 * @see IResource#delete(int,IProgressMonitor)
	 * @see IMoveDeleteHook#deleteFile(IResourceTree,IFile,int,IProgressMonitor)
	 */
	public boolean deleteFile(
		IResourceTree tree,
		IFile file,
		int updateFlags,
		IProgressMonitor monitor) {

		// do nothing if file does not exist in the workspace resource tree
		if (!file.exists()) {
			// return true anyway to say that the operation has been done
			return true;
		}

		// succeed immediately if file does not exist in the local file system
		java.io.File lfsFile = file.getLocation().toFile();
		if (!lfsFile.exists()) {
			// update the workspace resource tree to match
			tree.deletedFile(file);
			// return true to say that the operation has been done
			return true;
		}

		// if FORCE is not specified, fail if the workspace resource tree is
		// not in sync with file in the local file system
		boolean force = (updateFlags & IResource.FORCE) != 0;
		if (!force) {
			boolean inSync = tree.isSynchronized(file, IResource.DEPTH_ZERO);
			if (!inSync) {
				// report failure
				Status status =
					new Status(
						Status.ERROR,
						"com.example.movedeletehook",
						0,
						"File " + file.getFullPath() + " is out of sync with the local file system",
						null);
				tree.failed(status);
				// return true to say that the operation has been done
				return true;
			}
		}

		// capture the current state of the file in the local history if
		// KEEP_HISTORY is specified
		boolean keepHistory = (updateFlags & IResource.KEEP_HISTORY) != 0;
		if (keepHistory) {
			tree.addToLocalHistory(file);
		}

		// try to delete the file from the local file system
		boolean success = lfsFile.delete();
		if (success) {
			// update the workspace resource tree to match
			tree.deletedFile(file);
			// return true to say that the operation has been done
			return true;
		} else {
			// report an unexpected failure
			Status status =
				new Status(
					Status.ERROR,
					"com.example.movedeletehook",
					0,
					"Unable to delete file " + file.getFullPath() + " from the local file system",
					null);
			tree.failed(status);
			// return true to say that the operation has been done
			return true;
		}
	}

	/**
	 * This <code>IMoveDeleteHook</code> method implements 
	 * <code>IResource.delete(int,IProgressMonitor)</code> where the receiver is
	 * a folder. This example implementation illustrates the steps involved 
	 * (except for progress monitoring). The general approach illustrated here
	 * first deletes the entire subtree in the local file system and then fixes
	 * up the workspace resource tree to match. (There other approach is to
	 * carry out the deletion in step.)
	 * 
	 * @param tree the workspace resource tree; this object is only valid 
	 *    for the duration of the invocation of this method, and must not be 
	 *    used after this method has completed
	 * @param folder the handle of the folder to delete; the receiver of
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @param updateFlags bit-wise or of update flag constants as per 
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @param monitor the progress monitor, or <code>null</code> as per 
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @return <code>false</code> if this method declined to assume 
	 *   responsibility for this operation, and <code>true</code> if this
	 *   method attempted to carry out the operation
	 * @see IResource#delete(int,IProgressMonitor)
	 * @see IMoveDeleteHook#deleteFolder(IResourceTree,IFolder,int,IProgressMonitor)
	 */
	public boolean deleteFolder(
		IResourceTree tree,
		IFolder folder,
		int updateFlags,
		IProgressMonitor monitor) {

		// do nothing if folder does not exist in the workspace resource tree
		if (!folder.exists()) {
			// return true to say that the operation has been done
			return true;
		}

		// succeed immediately if folder does not exist in the local file system
		java.io.File lfsFolder = folder.getLocation().toFile();
		if (!lfsFolder.exists()) {
			// update the workspace resource tree to match
			tree.deletedFolder(folder);
			// return true to say that the operation has been done
			return true;
		}

		// if FORCE is not specified, fail if the workspace resource tree is 
		// not in sync with the directory subtree in the local file 
		// system (depth infinity)
		boolean force = (updateFlags & IResource.FORCE) != 0;
		if (!force) {
			boolean inSync = tree.isSynchronized(folder, IResource.DEPTH_INFINITE);
			if (!inSync) {
				// report failure
				Status status =
					new Status(
						Status.ERROR,
						"com.example.movedeletehook",
						0,
						"Folder "
							+ folder.getFullPath()
							+ " or one of its descendents is out of sync with the local file system",
						null);
				tree.failed(status);
				// return true to say that the operation has been done
				return true;
			}
		}

		// capture the current state of all files in the local history if
		// KEEP_HISTORY is specified
		boolean keepHistory = (updateFlags & IResource.KEEP_HISTORY) != 0;
		if (keepHistory) {
			addAllFilesToHistory(tree, folder);
		}

		// try to delete the subtree in the local file system
		boolean lfsSuccess = deleteLocalSubtree(lfsFolder);
		if (lfsSuccess) {
			// update the workspace resource tree to match
			tree.deletedFolder(folder);
			// return true to say that the operation has been done
			return true;
		} else {
			// report an unexpected failure
			Status status =
				new Status(
					Status.ERROR,
					"com.example.movedeletehook",
					0,
					"Unable to delete folder "
						+ folder.getFullPath()
						+ " from the local file system",
					null);
			tree.failed(status);
			// prune out any resources in the workspace resource tree that are gone
			pruneMissingResources(tree, folder);
			// return true to say that the operation has been done
			return true;
		}
	}

	/**
	 * Deletes resources from the workspace resource tree below the given
	 * container if they do not exist in the local file system. The given
	 * container itself is not affected.
	 * 
	 * @param tree the workspace resource tree
	 * @param container the root container (a folder or project)
	 */
	private void pruneMissingResources(IResourceTree tree, IContainer container) {
		IResource[] children;
		try {
			// we're interested in all members, including team-private ones
			children = container.members(IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS);
		} catch (CoreException e) {
			// bail quietly if there are problem with accessing its members
			return;
		}
		// iterate over children looking for ones to prune			
		for (int i = 0; i < children.length; i++) {
			IResource child = children[i];
			// does child exist in the local file system?
			java.io.File lfsChild = child.getLocation().toFile();
			if (!lfsChild.exists()) {
				// child no longer exists in local file system
				switch (child.getType()) {
					case IResource.FILE :
						// update workspace resource tree to say we deleted it
						tree.deletedFile((IFile) child);
						break;
					case IResource.FOLDER :
						// update workspace resource tree to say we deleted it
						tree.deletedFolder((IFolder) child);
						break;
					case IResource.PROJECT :
						// can't happen since container is a folder or project
						break;
					case IResource.ROOT :
						// can't happen since container is a folder or project
						break;
				}
			} else {
				// child exists in local file system
				if (child.getType() == IResource.FOLDER) {
					// recurse to prune inside a child subfolder
					pruneMissingResources(tree, (IFolder) child);
				}
			}
		}
	}

	/**
	 * This <code>IMoveDeleteHook</code> method implements 
	 * <code>IResource.delete(int,IProgressMonitor)</code> where the receiver is
	 * a project. This example implementation illustrates the steps involved 
	 * (except for progress monitoring).
	 * 
	 * @param tree the workspace resource tree; this object is only valid 
	 *    for the duration of the invocation of this method, and must not be 
	 *    used after this method has completed
	 * @param project the handle of the project to delete; the receiver of
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @param updateFlags bit-wise or of update flag constants as per 
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @param monitor the progress monitor, or <code>null</code> as per 
	 *    <code>IResource.delete(int,IProgressMonitor)</code>
	 * @return <code>false</code> if this method declined to assume 
	 *   responsibility for this operation, and <code>true</code> if this 
	 *   method attempted to carry out the operation
	 * @see IResource#delete(int,IProgressMonitor)
	 * @see IMoveDeleteHook#deleteProject(IResourceTree, IProject, int, IProgressMonitor)
	 */
	public boolean deleteProject(
		IResourceTree tree,
		IProject project,
		int updateFlags,
		IProgressMonitor monitor) {

		// do nothing if the project does not exist in the workspace resource tree
		if (!project.exists()) {
			// return true to say that the operation has been done
			return true;
		}

		// succeed immediately if project content area does not exist in the local file system
		java.io.File lfsProjectContentArea = project.getLocation().toFile();
		if (!lfsProjectContentArea.exists()) {
			// update the workspace resource tree to match
			tree.deletedProject(project);
			// return true to say that the operation has been done
			return true;
		}

		// decide if files in project content area are supposed to get deleted
		boolean alwaysDelete =
			(updateFlags & IResource.ALWAYS_DELETE_PROJECT_CONTENT) != 0;
		boolean neverDelete =
			(updateFlags & IResource.NEVER_DELETE_PROJECT_CONTENT) != 0;
		boolean force = (updateFlags & IResource.FORCE) != 0;
		boolean deletingContent;
		if (neverDelete) {
			deletingContent = false;
		} else if (alwaysDelete) {
			// ALWAYS_DELETE_PROJECT_CONTENT implies FORCE
			force = true;
			deletingContent = true;
		} else {
			// default: delete content area for open projects but not closed ones
			deletingContent = project.isOpen();
		}

		// if deleting the project content area and FORCE is not specified 
		// (and ALWAYS_DELETE_PROJECT_CONTENT, which implies FORCE)
		// fail if the workspace resource tree is not in sync with the
		// directory subtree in the local file system (depth infinity)
		if (deletingContent & !force) {
			boolean inSync = tree.isSynchronized(project, IResource.DEPTH_INFINITE);
			if (!inSync) {
				// report failure
				Status status =
					new Status(
						Status.ERROR,
						"com.example.movedeletehook",
						0,
						"Project "
							+ project.getFullPath()
							+ " is out of sync with the local file system",
						null);
				tree.failed(status);
				// return true to say that the operation has been done
				return true;
			}
		}

		// n.b. never capture local history when a project is being deleted
		// because history is kept with the project and gets deleted too

		if (!deletingContent) {
			// delete the project from the workspace resource tree
			tree.deletedProject(project);
			// return true to say that the operation has been done
			return true;
		}

		// try to delete the project content area in the local file system
		boolean lfsSuccess = deleteLocalSubtree(lfsProjectContentArea);
		if (lfsSuccess) {
			// if successful delete the project from the workspace resource tree
			tree.deletedProject(project);
			// return true to say that the operation has been done
			return true;
		} else {
			// report an unexpected failure
			Status status =
				new Status(
					Status.ERROR,
					"com.example.movedeletehook",
					0,
					"Unable to delete project "
						+ project.getFullPath()
						+ " from the local file system",
					null);
			tree.failed(status);
			// prune out any resources in the workspace resource tree that are gone
			pruneMissingResources(tree, project);
			// return true to say that the operation has been done
			return true;
		}
	}

	/**
	 * This <code>IMoveDeleteHook</code> method implements 
	 * <code>IResource.move(IPath,int,IProgressMonitor)</code> where the receiver
	 * is a file. This example implementation illustrates the steps involved 
	 * (except for progress monitoring).
	 * 
	 * @param tree the workspace resource tree; this object is only valid 
	 *    for the duration of the invocation of this method, and must not be 
	 *    used after this method has completed
	 * @param source the handle of the file to move; the receiver of
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>; 
	 *    guaranteed to exist in the workspace resource tree
	 * @param destination the handle of where the file will move to; the handle 
	 *    equivalent of the first parameter to
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>;
	 *    guaranteed to not exist in the workspace resource tree;
	 *    parent container guaranteed to exist and be open
	 * @param updateFlags bit-wise or of update flag constants as per 
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>
	 * @param monitor the progress monitor, or <code>null</code> as per 
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>
	 * @return <code>false</code> if this method declined to assume 
	 *   responsibility for this operation, and <code>true</code> if this
	 *   method attempted to carry out the operation
	 * @see IResource#move(IPath,int,IProgressMonitor)
	 * @see IMoveDeleteHook#moveFile(IResourceTree,IFile,IFile,int,IProgressMonitor)
	 */
	public boolean moveFile(
		IResourceTree tree,
		IFile source,
		IFile destination,
		int updateFlags,
		IProgressMonitor monitor) {

		// Given: source exists in workspace resource tree
		// Given: destination does not exist in workspace resource tree
		// Given: destination parent exists and is open in workspace resource tree
		if (!source.exists()
			|| destination.exists()
			|| !destination.getParent().isAccessible()) {
			throw new IllegalArgumentException();
		}

		// if FORCE is not specified, fail if the workspace resource tree is
		// not in sync at source file in the local file system
		boolean force = (updateFlags & IResource.FORCE) != 0;
		if (!force) {
			boolean inSync = tree.isSynchronized(source, IResource.DEPTH_ZERO);
			if (!inSync) {
				// report failure
				Status status =
					new Status(
						Status.ERROR,
						"com.example.movedeletehook",
						0,
						"File " + source.getFullPath() + " is out of sync with the local file system",
						null);
				tree.failed(status);
				// return true to say that the operation has been done
				return true;
			}
		}

		// capture the current state of the source file in the local history if
		// KEEP_HISTORY is specified
		boolean keepHistory = (updateFlags & IResource.KEEP_HISTORY) != 0;
		if (keepHistory) {
			tree.addToLocalHistory(source);
		}

		// try to move the source file in the local file system
		java.io.File lfsSource = source.getLocation().toFile();
		java.io.File lfsDestination = destination.getLocation().toFile();
		boolean moveSuccess = moveLocalFile(lfsSource, lfsDestination);
		if (moveSuccess) {
			// update the workspace resource tree to match
			tree.movedFile(source, destination);
			// moveLocalFile may have affected timestamp
			// update timestamp to avoid having out of sync destination
			tree.updateMovedFileTimestamp(destination, tree.computeTimestamp(destination));
			// return true to say that the operation has been done
			return true;
		} else {
			// report an unexpected failure
			Status status =
				new Status(
					Status.ERROR,
					"com.example.movedeletehook",
					0,
					"Unable to move file " + source.getFullPath() + " in the local file system",
					null);
			tree.failed(status);
			// return true to say that the operation has been done
			return true;
		}
	}

	/**
	 * This <code>IMoveDeleteHook</code> method implements 
	 * <code>IResource.move(IPath,int,IProgressMonitor)</code> where the receiver
	 * is a folder. This example implementation illustrates the steps involved 
	 * (except for progress monitoring). The general approach illustrated here
	 * moves the entire subtree in the local file system and then fixes
	 * up the workspace resource tree to match using
	 * <code>IResourceTree.movedFolder</code>.
	 * 
	 * @param tree the workspace resource tree; this object is only valid 
	 *    for the duration of the invocation of this method, and must not be 
	 *    used after this method has completed
	 * @param source the handle of the folder to move; the receiver of
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>;
	 *    guaranteed to exist in the workspace resource tree
	 * @param destination the handle of where the folder will move to; the 
	 *    handle equivalent of the first parameter to
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>
	 *    guaranteed to not exist in the workspace resource tree;
	 *    parent container guaranteed to exist and be open
	 * @param updateFlags bit-wise or of update flag constants as per 
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>
	 * @param monitor the progress monitor, or <code>null</code> as per 
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>
	 * @return <code>false</code> if this method declined to assume 
	 *   responsibility for this operation, and <code>true</code> if this 
	 *   method attempted to carry out the operation
	 * @see IResource#move(IPath,int,IProgressMonitor)
	 * @see IMoveDeleteHook#moveFolder(IResourceTree,IFolder,IFolder,int,IProgressMonitor)
	 */
	public boolean moveFolder(
		IResourceTree tree,
		IFolder source,
		IFolder destination,
		int updateFlags,
		IProgressMonitor monitor) {

		// Given: source exists in workspace resource tree
		// Given: destination does not exist in workspace resource tree
		// Given: destination parent exists and is open in workspace resource tree
		if (!source.exists()
			|| destination.exists()
			|| !destination.getParent().isAccessible()) {
			throw new IllegalArgumentException();
		}

		// if FORCE is not specified, fail if the workspace resource tree is
		// not in sync at source folder and its descendents in the local file system
		boolean force = (updateFlags & IResource.FORCE) != 0;
		if (!force) {
			boolean inSync = tree.isSynchronized(source, IResource.DEPTH_INFINITE);
			if (!inSync) {
				// report failure
				Status status =
					new Status(
						Status.ERROR,
						"com.example.movedeletehook",
						0,
						"Folder " + source.getFullPath() + " is out of sync with the local file system",
						null);
				tree.failed(status);
				// return true to say that the operation has been done
				return true;
			}
		}

		// capture the current state of all files in the local history if
		// KEEP_HISTORY is specified
		boolean keepHistory = (updateFlags & IResource.KEEP_HISTORY) != 0;
		if (keepHistory) {
			addAllFilesToHistory(tree, source);
		}

		// try to move the subtree in the local file system
		java.io.File lfsSource = source.getLocation().toFile();
		java.io.File lfsDestination = destination.getLocation().toFile();
		boolean lfsSuccess = moveLocalSubtree(lfsSource, lfsDestination);
		if (lfsSuccess) {
			// update the workspace resource tree to match
			tree.movedFolderSubtree(source, destination);
			// moveLocalSubtree may have affected file timestamps
			// update file timestamps to avoid having out of sync destination
			updateTimestamps(tree, destination);
			// return true to say that the operation has been done
			return true;
		} else {
			// report an unexpected failure
			Status status =
				new Status(
					Status.ERROR,
					"com.example.movedeletehook",
					0,
					"Unable to move folder " + source.getFullPath() + " in the local file system",
					null);
			tree.failed(status);
			// return true to say that the operation has been done
			return true;
		}
	}

	/**
	 * Updates timestamps for all files in the workspace resource subtree rooted
	 * at the given container. The given container must exist and be accessible.
	 * <p>
	 * Note that this is <b>not</b> the same thing as <code>refreshLocal</code>,
	 * because (a) it only updates file timestamps for file known to the
	 * workspace resource tree, and (b) the updated timestamps are not 
	 * considered to be changes.
	 * 
	 * @param tree the workspace resource tree
	 * @param container the root container
	 */
	private void updateTimestamps(final IResourceTree tree, IContainer container) {

		// Resource visitor for updtating timestamp for files it visits
		class UpdateVisitor implements IResourceVisitor {
			public boolean visit(IResource resource) {
				if (resource.getType() == IResource.FILE) {
					IFile file = (IFile) resource;
					tree.updateMovedFileTimestamp(file, tree.computeTimestamp(file));
				}
				return true;
			}
		}

		try {
			// update timestamps for both regular and team-private members
			container.accept(
				new UpdateVisitor(),
				IResource.DEPTH_INFINITE,
				IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS);
		} catch (CoreException e) {
			// visitor does not throw CoreException
			// container is known to be accessible
			// exception should not happen
			throw new RuntimeException();
		}
	}

	/**
	 * This <code>IMoveDeleteHook</code> method implements 
	 * <code>IResource.move(IProjectDescrition,int,IProgressMonitor)</code> 
	 * where the receiver is a project. This example implementation illustrates
	 * the steps involved (except for progress monitoring). The general approach
	 * illustrated here relocates the entire project content are in the local
	 * file system and then fixes up the workspace resource tree to match using
	 * <code>IResourceTree.movedProject</code>.
	 * 
	 * @param tree the workspace resource tree; this object is only valid 
	 *    for the duration of the invocation of this method, and must not be 
	 *    used after this method has completed
	 * @param source the handle of the project to move; the receiver of
	 *    <code>IResource.move(IProjectDescription,int,IProgressMonitor)</code>
	 *    or <code>IResource.move(IPath,int,IProgressMonitor)</code>;
	 *    guaranteed to exist and be open in the workspace resource tree
	 * @param description the new description of the project; the first
	 *    parameter to
	 *    <code>IResource.move(IProjectDescription,int,IProgressMonitor)</code>, or
	 *    a copy of the project's description with the location changed to the
	 *    path given in the first parameter to 
	 *    <code>IResource.move(IPath,int,IProgressMonitor)</code>
	 * @param updateFlags bit-wise or of update flag constants as per 
	 *    <code>IResource.move(IProjectDescription,int,IProgressMonitor)</code>
	 *    or <code>IResource.move(IPath,int,IProgressMonitor)</code>
	 * @param monitor the progress monitor, or <code>null</code> as per 
	 *    <code>IResource.move(IProjectDescription,int,IProgressMonitor)</code>
	 *    or <code>IResource.move(IPath,int,IProgressMonitor)</code>
	 * @return <code>false</code> if this method declined to assume 
	 *   responsibility for this operation, and <code>true</code> if this method
	 *   attempted to carry out the operation
	 * @see IResource#move(IPath,int,IProgressMonitor)
	 * @see IResource#move(IProjectDescription,int,IProgressMonitor)
	 * @see IMoveDeleteHook#moveProject(IResourceTree,IProject,IProjectDescription,int,IProgressMonitor)
	 */
	public boolean moveProject(
		IResourceTree tree,
		IProject project,
		IProjectDescription description,
		int updateFlags,
		IProgressMonitor monitor) {

		// Given: source exists and is open in workspace resource tree
		if (!project.exists() || !project.isOpen()) {
			throw new IllegalArgumentException();
		}

		// are we changing the name of the project?
		// this affects the workspace resource tree
		String oldProjectName = project.getName();
		String newProjectName = description.getName();
		boolean renaming = !newProjectName.equals(oldProjectName);

		// are we changing the location of the project content area?
		// this affects the files in the local file system 
		// and the workspace resource tree
		IPath oldProjectContentArea = project.getLocation();
		IPath newProjectContentArea = description.getLocation();
		if (newProjectContentArea == null) {
			// compute path of new default project content area
			newProjectContentArea = Platform.getLocation().append(newProjectName);
		}
		boolean relocating = !newProjectContentArea.equals(oldProjectContentArea);

		IProject newProject =
			project.getWorkspace().getRoot().getProject(newProjectName);

		if (!renaming && !relocating) {
			// the hook method should never have been called
			throw new IllegalArgumentException();
		}

		// fail we are renaming to project that exists in workspace resource tree
		if (renaming && newProject.exists()) {
			// report failure
			Status status =
				new Status(
					Status.ERROR,
					"com.example.movedeletehook",
					0,
					"Project " + newProject.getFullPath() + " already exists",
					null);
			tree.failed(status);
			// return true to say that the operation has been done
			return true;
		}

		// relocate the project content area
		if (relocating) {
			// need to move files in local file system
			boolean lfsMoveSuccess =
				moveLocalSubtree(
					oldProjectContentArea.toFile(),
					newProjectContentArea.toFile());
			if (!lfsMoveSuccess) {
				// report failure
				Status status =
					new Status(
						Status.ERROR,
						"com.example.movedeletehook",
						0,
						"Unable to move project contents for " + newProject.getFullPath(),
						null);
				tree.failed(status);
				// return true to say that the operation has been done
				return true;
			}
		}

		// update project in workspace resource tree
		boolean treeMoveSuccess = tree.movedProjectSubtree(project, description);
		if (!treeMoveSuccess) {
			// report failure
			Status status =
				new Status(
					Status.ERROR,
					"com.example.movedeletehook",
					0,
					"Unable to move project " + project.getFullPath(),
					null);
			tree.failed(status);
			// return true to say that the operation has been done
			return true;
		}
		
		if (relocating) {
			// moveLocalSubtree may have affected file timestamps
			// update file timestamps to avoid having out of sync destination
			updateTimestamps(tree, newProject);
		}
		// return true to say that the operation has been done
		return true;
	}

	/**
	 * Captures local history for all files in the subtree rooted at the 
	 * given container. The given container must exist and be accessible.
	 * 
	 * @param tree the workspace resource tree
	 * @param container the root container
	 */
	private void addAllFilesToHistory(
		final IResourceTree tree,
		IContainer container) {

		// Resource visitor for keeping local listory for files it visits
		class KeepVisitor implements IResourceVisitor {
			public boolean visit(IResource resource) {
				if (resource.getType() == IResource.FILE) {
					// capture current state of file in local history
					tree.addToLocalHistory((IFile) resource);
				}
				return true;
			}
		}

		try {
			// only save history for regular members as there is little point
			// in saving history for team-private members
			container.accept(new KeepVisitor(), IResource.DEPTH_INFINITE, IResource.NONE);
		} catch (CoreException e) {
			// visitor does not throw CoreException
			// container is known to be accessible
			// exception should not happen
			throw new RuntimeException();
		}
	}

	/**
	 * Deletes the given directory subtree in the local file system.
	 * Returns <code>true</code> immediately if the given folder does
	 * not exist.
	 * 
	 * @param lfsFolder the folder in the local file system
	 * @return <code>true</code> if the given folder no longer exists,
	 *    and <code>false</code> if it was not deleted
	 */
	private boolean deleteLocalSubtree(java.io.File lfsFolder) {
		java.io.File[] lfsChildren = lfsFolder.listFiles();
		if (lfsChildren == null) {
			if (lfsFolder.exists()) {
				// folder does exists (I/O error or not a directory)
				return false;
			} else {
				// folder does not exist
				return true;
			}
		}
		// delete all children first
		for (int i = 0; i < lfsChildren.length; i++) {
			java.io.File lfsChild = lfsChildren[i];
			if (lfsChild.isFile()) {
				// attempt to delete the file
				boolean childSuccess = lfsChild.delete();
				// don't worry if we could not delete the child file
				// we will be unable to delete the parent folder if it still
				// has children
			} else if (lfsChild.isDirectory()) {
				// otherwise recurse over the child subtree
				boolean childSuccess = deleteLocalSubtree(lfsChild);
				// don't worry if we could not delete the child folder
				// we will be unable to delete the parent folder if it still
				// has children
			}
		}
		// delete folder now that children should be gone
		boolean success = lfsFolder.delete();
		// we're happy as long as folder is gone
		return !lfsFolder.exists();
	}

	/**
	 * Copies the given source file in the local file system to the given
	 * destination file. The operation fails rather than overwriting an
	 * existing destination file. The destination parent folder will be
	 * created if required.
	 * 
	 * @param lfsSource the source file in the local file system; this is
	 *    the file to be copied
	 * @param lfsDestination the destination file in the local file system;
	 *    this is where the file is to be copied to
	 * @return <code>true</code> if the copy was successful,
	 *    and <code>false</code> if the copy failed
	 */
	private boolean copyLocalFile(
		java.io.File lfsSource,
		java.io.File lfsDestination) {

		// create the destination parent folder if required
		java.io.File lfsDestinationParent = lfsDestination.getParentFile();
		if (lfsDestinationParent != null && !lfsDestinationParent.exists()) {
			boolean mkdirSuccess = lfsDestinationParent.mkdirs();
			if (!mkdirSuccess) {
				// fail if unable to create destination parent folder
				return false;
			}
		}

		if (lfsDestination.exists()) {
			// so that we do not overwrite an existing file
			return false;
		}

		InputStream in = null;
		OutputStream out = null;
		try {
			in = new BufferedInputStream(new FileInputStream(lfsSource));
			out = new BufferedOutputStream(new FileOutputStream(lfsDestination));
			long fileSize = lfsSource.length();
			// use 10KB buffer for small files
			int bufferSize = 10 * 1024;
			if (fileSize > 100 * 1024) {
				// use 100KB buffer for medium-sized files
				bufferSize = 100 * 1024;
			}
			if (fileSize > 1000 * 1024) {
				// use 1MB buffer for large files
				bufferSize = 1000 * 1024;
			}
			byte[] buffer = new byte[bufferSize];
			while (true) {
				int bytesRead = in.read(buffer);
				if (bytesRead < 0) {
					break;
				}
				out.write(buffer, 0, bytesRead);
			}
			return true;
		} catch (FileNotFoundException e) {
			// unable to open the source file for input
			// or unable to open the destination file for output
			// fail in either case
			return false;
		} catch (IOException e) {
			// fail
			return false;
		} finally {
			// close both streams in all cases
			try {
				if (in != null) {
					in.close();
				}
			} catch (IOException closeException) {
				// ignore
			}
			try {
				if (out != null) {
					out.close();
				}
			} catch (IOException closeException) {
				// better safe than sorry
				// fail if we have problems closing the output stream
				return false;
			}
		}
	}

	/**
	 * Copies the given folder subtree in the local file system to the given
	 * destination folder. The operation fails rather than overwriting an
	 * existing file. The destination parent folder will be created if required.
	 * 
	 * @param lfsSource the source folder in the local file system; this is
	 *    the folder to be copied
	 * @param lfsDestination the destination folder in the local file system;
	 *    this is where the folder is to be copied to
	 * @return <code>true</code> if the copy was successful,
	 *    and <code>false</code> if the copy failed
	 */
	private boolean copyLocalSubtree(
		java.io.File lfsSource,
		java.io.File lfsDestination) {

		java.io.File[] lfsChildren = lfsSource.listFiles();
		if (lfsChildren == null) {
			// folder does not exist, I/O error, or not a directory
			// fail since no source to copy
			return false;
		}

		// create the destination folder if required
		if (!lfsDestination.exists()) {
			boolean mkdirSuccess = lfsDestination.mkdirs();
			if (!mkdirSuccess) {
				// fail if unable to create destination folder
				return false;
			}
		}

		// copy all children
		for (int i = 0; i < lfsChildren.length; i++) {
			java.io.File lfsChild = lfsChildren[i];
			java.io.File lfsDestinationChild =
				new java.io.File(lfsDestination, lfsChild.getName());
			boolean childSuccess = false;
			if (lfsChild.isFile()) {
				// attempt to copy the file
				childSuccess = copyLocalFile(lfsChild, lfsDestinationChild);
			} else if (lfsChild.isDirectory()) {
				// otherwise recursively copy the child folder
				childSuccess = copyLocalSubtree(lfsChild, lfsDestinationChild);
			}
			if (!childSuccess) {
				// fail immediately if unable to successfully copy a child
				return false;
			}
		}
		// succeed since all children were successfully copied
		return true;
	}

	/**
	 * Moves the given source file in the local file system to the given
	 * destination file. The destination's parent folder will be created
	 * if required. The timestamp of the file may change in the process.
	 * 
	 * @param lfsSource the source file in the local file system; this is
	 *    the file to be moved
	 * @param lfsDestination the destination file in the local file system;
	 *    this is where the file is to be moved to
	 * @return <code>true</code> if the move was successful,
	 *    and <code>false</code> if the move failed
	 */
	private boolean moveLocalFile(
		java.io.File lfsSource,
		java.io.File lfsDestination) {

		// create the destination parent (and ancestors) if required
		java.io.File lfsDestinationParent = lfsDestination.getParentFile();
		if (lfsDestinationParent != null && !lfsDestinationParent.exists()) {
			boolean mkdirsSuccess = lfsDestinationParent.mkdirs();
			if (!mkdirsSuccess) {
				// fail because destination parent cannot be created
				return false;
			}
		}

		// attempt to rename the file
		boolean renameSuccess = lfsSource.renameTo(lfsDestination);
		if (renameSuccess) {
			// that was easy
			return true;
		}

		// plan B: copy the file and delete the original
		boolean copySuccess = copyLocalFile(lfsSource, lfsDestination);
		if (!copySuccess) {
			// fail if unable to make copy
			return false;
		}

		// delete the source file
		boolean deleteSuccess = lfsSource.delete();
		// operation succeeds iff we copied source and successfully deleted it
		return deleteSuccess;
	}

	/**
	 * Moves the given subtree in the local file system to the given
	 * destination. The destination's parent folder will be created
	 * if required.
	 * 
	 * @param lfsSource the source folder in the local file system; this is
	 *    the folder to be moved
	 * @param lfsDestination the destination folder in the local file system;
	 *    this is where the folder is to be moved to
	 * @return <code>true</code> if the move was successful,
	 *    and <code>false</code> if the move failed
	 */
	private boolean moveLocalSubtree(
		java.io.File lfsSource,
		java.io.File lfsDestination) {

		// create the destination parent (and ancestors) if required
		java.io.File lfsDestinationParent = lfsDestination.getParentFile();
		if (lfsDestinationParent != null && !lfsDestinationParent.exists()) {
			boolean mkdirsSuccess = lfsDestinationParent.mkdirs();
			if (!mkdirsSuccess) {
				// fail because destination parent cannot be created
				return false;
			}
		}

		// attempt to rename the folder
		boolean renameSuccess = lfsSource.renameTo(lfsDestination);
		if (renameSuccess) {
			// that was easy
			return true;
		}

		// plan B: copy the folder and then delete the original
		boolean copySuccess = copyLocalSubtree(lfsSource, lfsDestination);
		if (!copySuccess) {
			// fail if unable to make copy
			return false;
		}

		// delete the source folder
		boolean deleteSuccess = deleteLocalSubtree(lfsSource);
		// operation succeeds iff we copied source and successfully deleted it
		return deleteSuccess;
	}
}