./codecover/src/org/codecover/eclipse/tscmanager/TSContainerStorage.java

/******************************************************************************
 * Copyright (c) 2007 Stefan Franke, Robert Hanussek, Benjamin Keil,          *
 *                    Steffen Kieß, Johannes Langauf,                         *
 *                    Christoph Marian Müller, Igor Podolskiy,                *
 *                    Tilmann Scheller, Michael Starzmann, Markus Wittlinger  *
 * 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                                  *
 ******************************************************************************/

package org.codecover.eclipse.tscmanager;

import java.io.ByteArrayInputStream;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.codecover.eclipse.CodeCoverPlugin;
import org.codecover.eclipse.Messages;
import org.codecover.eclipse.tscmanager.exceptions.CancelException;
import org.codecover.eclipse.tscmanager.exceptions.TSCFileCreateException;
import org.codecover.model.MASTBuilder;
import org.codecover.model.TestSessionContainer;
import org.codecover.model.exceptions.FileLoadException;
import org.codecover.model.exceptions.FileSaveException;
import org.codecover.model.utils.Logger;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceRuleFactory;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.MultiRule;

/**
 * Handles the saving and loading of test session containers. Locking of the stored and related objects (i.e.,
 * the method parameters) must be done by the calling context.
 * 
 * @author Robert Hanussek
 * @version 1.0 ($Id: TSContainerStorage.java 49 2009-06-01 08:52:19Z ahija $)
 */
class TSContainerStorage {

  private static final String MONITOR_WRITING_TEST_SESSION_CONTAINER =
    Messages.getString("TSContainerStorage.MONITOR_WRITING_TEST_SESSION_CONTAINER"); //$NON-NLS-1$

  private static final String MONITOR_LOADING_TEST_SESSION_CONTAINER_FROM =
    Messages.getString("TSContainerManager.MONITOR_LOADING_TEST_SESSION_CONTAINER_FROM"); //$NON-NLS-1$

  private static final String MONITOR_DELETING_TSC_FILES =
    Messages.getString("TSContainerStorage.MONITOR_DELETING_TSC_FILES"); //$NON-NLS-1$

  private static final String TSC_FILENAME_PREFIX = "test-session-container"; //$NON-NLS-1$

  private static final String TSC_FILENAME_SEPERATOR = "-"; //$NON-NLS-1$

  private static final DateFormat TSC_FILENAME_DATE_SUFFIX = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS"); //$NON-NLS-1$

  private static final String TSC_FILENAME_EXTENSION = ".xml"; //$NON-NLS-1$

  private static final String OLD_TSC_FILENAME_EXTENSION = ".old";//$NON-NLS-1$

  private final TSContainerSaveQueue saveQueue;

  private final Logger logger;

  TSContainerStorage(TSContainerManager tscManager) {
    if (tscManager == null) {
      throw new NullPointerException("tscManager mustn't be null"); //$NON-NLS-1$
    }

    this.logger = tscManager.getLogger();
    this.saveQueue = new TSContainerSaveQueue(this);
  }

  /**
   * Returns (always the same) logger.
   * 
   * @return the logger, which doesn't change over time
   */
  Logger getLogger() {
    return this.logger;
  }

  /*
   * methods to save test session containers
   */

  TSContainerSaveQueue getSaveQueue() {
    return this.saveQueue;
  }

  /**
   * Locking of TSContainerManager must be done by the calling context.
   */
  void saveTSContainer(final TestSessionContainer tsc, final TSContainerInfo tscInfo, boolean queue,
    IProgressMonitor monitor) throws TSCFileCreateException, FileSaveException, OutOfMemoryError {
    if (queue) {
      this.saveQueue.queueSave(tscInfo, tsc);
    } else {
      this.writeTestSessionContainer(tsc, null, null, tscInfo, monitor);
    }
  }

  /**
   * Writes a test session container to a file in the CodeCover-folder of a project. Locking of
   * TSContainerManager must be done by the calling context if required (i.e., if a known test
   * session container is written).
   * 
   * @param tsc the test session container to write
   * @param reqFname the requested filename for the test session container; If it is null, a
   *        filename in a default format is used which incorporates the current date and time. If the
   *        requested or generated filename already exists in the folder and parameter
   *        findAlternative is true, the suffix .N is appended
   *        to the given filename. Whereas N is a number so that the extended
   *        filename.N is unique in the CodeCover-folder of the given project.
   * @param destProj the associated project, i.e. the project the test session container belongs to, its file
   *        is stored in the CodeCover-folder of the project which is created if it doesn't already exist
   * @param tscInfo if the test session container is already known and just has to be written to update its
   *        file (i.e., to save it), this parameter can be passed; the parameters for the requested filename
   *        and the project are ignored if this one is passed (i.e. if this one is non-null)
   * @param monitor a progress monitor or null if progress reporting is not desired
   * @return the file of the written test session container
   * @throws TSCFileCreateException if the file to write to couldn't be created
   * @throws FileSaveException if the test session container couldn't be written to the file
   * @throws OutOfMemoryError if the java virtual machine is out of memory
   */
  IFile writeTestSessionContainer(final TestSessionContainer tsc, final String reqFname,
    final IProject destProj, final TSContainerInfo tscInfo, IProgressMonitor monitor)
    throws TSCFileCreateException, FileSaveException, OutOfMemoryError {
    final String filename = (tscInfo != null) ? tscInfo.getFile().getName() : reqFname;
    final IProject project = (tscInfo != null) ? tscInfo.getProject() : destProj;
    IWorkspace workspace = ResourcesPlugin.getWorkspace();
    IResourceRuleFactory ruleFactory = workspace.getRuleFactory();
    // needs to be a list to get the new IFile out of the IWorkspaceRunnable
    final List newFile = new ArrayList();
    if (tsc == null) {
      throw new NullPointerException("tsc mustn't be null"); //$NON-NLS-1$
    }
    if (project == null && tscInfo == null) {
      throw new NullPointerException("a project or a TSContainerInfo must be" + //$NON-NLS-1$
        " passed (non-null)"); //$NON-NLS-1$
    }

    monitor = (monitor != null) ? monitor : new NullProgressMonitor();
    IWorkspaceRunnable writeRunnable = new IWorkspaceRunnable() {

      public void run(IProgressMonitor monitor) throws CoreException {
        // the temporary file the test session container is saved to
        IFile tempFile;
        // the old file after it was moved to the temp folder
        IFile oldFile = null;
        IFolder tempFolder;
        IFolder codeCoverFolder;
        logger.debug("Trying to write test session container" + //$NON-NLS-1$
          " with ID " + tsc.getId() + //$NON-NLS-1$
          " to project " + project.getName()); //$NON-NLS-1$
        try { // this try only ensures that the monitor is left done()
          monitor.beginTask(MONITOR_WRITING_TEST_SESSION_CONTAINER, 6);

          if (tscInfo != null) {
            /*
             * remove pending saves from queue to avoid overwriting of this save by a queued one
             */
            TSContainerStorage.this.saveQueue.removeSaveFromQueue(tscInfo);
          }

          // open project if necessary
          if (!project.isOpen()) {
            // may throw CoreException
            project.open(null);
          }

          // save to temporary file
          try {
            tempFile = generateTSCFile(filename, project, true);
          } catch (TSCFileCreateException e) {
            throw new CoreException(new Status(IStatus.ERROR, CodeCoverPlugin.PLUGIN_ID, IStatus.OK, e
              .getMessage(), e));
          }
          monitor.worked(1); // #1
          try {
            tsc.save(tempFile.getLocation().toFile());
          } catch (FileSaveException fse) {
            logger.error("Error while writing test session" + //$NON-NLS-1$
              " container to temporary file", fse); //$NON-NLS-1$
            try {
              tempFile.delete(true, null);
            } catch (CoreException ce) {
              logger.error("Error while deleting temporary" + //$NON-NLS-1$
                " file.", ce); //$NON-NLS-1$
            }
            throw new CoreException(new Status(IStatus.ERROR, CodeCoverPlugin.PLUGIN_ID, IStatus.OK, fse
              .getMessage(), fse));
          } catch (OutOfMemoryError oome) {
            logger.error("Out of memory while writing" + //$NON-NLS-1$
              " test session container to" + //$NON-NLS-1$
              " temporary file", //$NON-NLS-1$
              new InvocationTargetException(oome));
            try {
              tempFile.delete(true, null);
            } catch (CoreException ce) {
              logger.error("Error while deleting temporary" + //$NON-NLS-1$
                " file.", ce); //$NON-NLS-1$
            }
            throw new CoreException(new Status(IStatus.ERROR, CodeCoverPlugin.PLUGIN_ID, IStatus.OK, oome
              .getMessage(), oome));
          }
          monitor.worked(1); // #2

          // sync workspace with temporary file in the local file system
          try {
            tempFile.refreshLocal(IResource.DEPTH_ZERO, null);
          } catch (CoreException e) {
            logger.error("Couldn't refresh workspace for" + //$NON-NLS-1$
              " (written) temporary file: " //$NON-NLS-1$
              + tempFile.getFullPath().toString(), e);
          }
          monitor.worked(1); // #3

          /*
           * move old file to temp folder to be able to restore it if something goes wrong
           */
          if (tscInfo != null && tscInfo.getFile().exists()) {
            try {
              tempFolder = createTempFolder(project);
              oldFile = findNewFilename(filename + OLD_TSC_FILENAME_EXTENSION, tempFolder, true);
            } catch (TSCFileCreateException e) {
              throw new CoreException(new Status(IStatus.ERROR, CodeCoverPlugin.PLUGIN_ID, IStatus.OK, e
                .getMessage(), e));
            }
            tscInfo.getFile().move(oldFile.getFullPath(), false, null);
          }

          // move new file to (the root of) the CodeCover-folder
          try {
            codeCoverFolder = createCodeCoverFolder(project);
            newFile.add(findNewFilename(filename, codeCoverFolder, tscInfo == null));
          } catch (TSCFileCreateException e) {
            try {
              /*
               * restore old file (move it from temp folder back to the CodeCover-folder)
               */
              if (tscInfo != null && oldFile != null) {
                oldFile.move(tscInfo.getFile().getFullPath(), false, null);
              }
            } catch (Exception exception) {
              /*
               * catch all exceptions to be able to throw the CoreException
               */
            }
            throw new CoreException(new Status(IStatus.ERROR, CodeCoverPlugin.PLUGIN_ID, IStatus.OK, e
              .getMessage(), e));
          }
          // may throw CoreException
          tempFile.move(newFile.get(0).getFullPath(), false, null);
          if (tscInfo != null) {
            /*
             * just done for safety. the path of the file doesn't (and mustn't) change, but maybe an IFile
             * isn't just defined through its path
             */
            tscInfo.setFile(newFile.get(0));
            tscInfo.setSynchronized(true);
          }
          monitor.worked(1); // #4

          // delete old file from temp folder
          if (oldFile != null) {
            try {
              oldFile.delete(true, null);
            } catch (CoreException ce) {
              logger.error("Error while deleting old" + //$NON-NLS-1$
                " file from temporary folder.", ce); //$NON-NLS-1$
            }
          }
          monitor.worked(1); // #5

          // sync workspace with saved file in the local file system
          try {
            newFile.get(0).refreshLocal(IResource.DEPTH_ZERO, null);
          } catch (CoreException e) {
            logger.warning("Couldn't refresh workspace for" + //$NON-NLS-1$
              " written file: " //$NON-NLS-1$
              + newFile.get(0).toString(), e);
          }
          monitor.worked(1); // #6

          logger.info("Wrote test session container: " //$NON-NLS-1$
            + newFile.get(0).getFullPath().toString());
        } finally {
          monitor.done();
        }
      }
    };

    try {
      /*
       * combine the rule for opening the project and the rule (which is the project itself) for modifying the
       * content of the project
       */
      workspace.run(writeRunnable, MultiRule.combine(ruleFactory.modifyRule(project), project),
        IWorkspace.AVOID_UPDATE, monitor);
    } catch (CoreException e) {
      if (e.getCause() != null) {
        if (e.getCause() instanceof TSCFileCreateException) {
          throw (TSCFileCreateException) e.getCause();
        } else if (e.getCause() instanceof FileSaveException) {
          throw (FileSaveException) e.getCause();
        } else if (e.getCause() instanceof OutOfMemoryError) {
          throw (OutOfMemoryError) e.getCause();
        }
      }
      throw new FileSaveException("Error while writing test session container: " //$NON-NLS-1$
        + filename, e);
    }
    return newFile.get(0);
  }

  /**
   * Generates a file in the CodeCover-folder or the folder for temporary files of CodeCover.
   * 
   * @param requestedFilename the requested filename, if null or empty a default format is
   *        used; If this filename already exists in the folder, the suffix .N is appended to
   *        the given filename. Whereas N is a number so that the extended
   *        filename.N is unique in the CodeCover-folder of the given project.
   * @param project the project the created file belongs to
   * @param temporary true if a temporary file is to be create, false otherwise
   * @return a new file in the CodeCover-folder or the folder for temporary files of CodeCover, whereas the
   *         file is associated with the given project by CodeCover
   * @throws TSCFileCreateException if the file or a containing folder could not be created
   */
  private static IFile generateTSCFile(String requestedFilename, IProject project, boolean temporary)
    throws TSCFileCreateException {
    IFolder codeCoverFolder;
    IFolder tempFolder;
    IFolder folder;
    IFile reqFile;

    // create CodeCover-folder if it doesn't exist
    // may throw TSCFileCreateException
    codeCoverFolder = createCodeCoverFolder(project);
    if (temporary) {
      // create folder for temporary files if it doesn't exist
      // may throw TSCFileCreateException
      tempFolder = createTempFolder(project);
      folder = tempFolder;
    } else {
      folder = codeCoverFolder;
    }

    // ensure that the file doesn't already exist
    reqFile = findNewFilename(requestedFilename, folder, true);

    // create the file
    try {
      reqFile.create(new ByteArrayInputStream(new byte[0]), false, null);
    } catch (CoreException e) {
      throw new TSCFileCreateException("Couldn't create file", e); //$NON-NLS-1$
    }

    return reqFile;
  }

  /**
   * Creates the CodeCover-folder of the given project if it doesn't exist already.
   * 
   * @param project the project the folder to create belongs to
   */
  private static IFolder createCodeCoverFolder(IProject project) throws TSCFileCreateException {
    IFolder codeCoverFolder = project.getFolder(CodeCoverPlugin.CODECOVER_FOLDER);
    if (!codeCoverFolder.exists()) {
      try {
        codeCoverFolder.create(false, true, null);
      } catch (CoreException e) {
        throw new TSCFileCreateException("Couldn't create CodeCover-folder", e); //$NON-NLS-1$
      }
    }
    return codeCoverFolder;
  }

  /**
   * Creates the folder for temporary files if it doesn't exist.
   * 
   * @param project the project the folder to create belongs to
   */
  private static IFolder createTempFolder(IProject project) throws TSCFileCreateException {
    IFolder codeCoverFolder;
    IFolder tempFolder;

    // may throw TSCFileCreateException
    codeCoverFolder = createCodeCoverFolder(project);

    tempFolder = codeCoverFolder.getFolder(CodeCoverPlugin.TEMP_FOLDER);

    // create folder for temporary files if it doesn't exist
    if (!tempFolder.exists()) {
      try {
        tempFolder.create(false, true, null);
      } catch (CoreException e) {
        throw new TSCFileCreateException("Couldn't create temporary folder", e);//$NON-NLS-1$
      }
    }
    return tempFolder;
  }

  /**
   * Finds an unused filename in the given folder.
   * 
   * @param requestedFilename the requested filename; If it is null, a filename in a default
   *        format is used which incorporates the current date and time. If the requested or generated
   *        filename already exists in the folder and parameter findAlternative is
   *        true, the suffix .N is appended to the given filename. Whereas
   *        N is a number so that the extended filename.N is unique in the given
   *        folder.
   * @param folder the folder to find an unused filename in
   * @param findAlternative if true and the requested filename already exists in the folder,
   *        this method tries to find an alternative name; if false, this method just checks
   *        if this filename is not already in use and throws an exception if this is the case
   * @return an unused filename in the given folder as an IFile object (the object just points
   *         to the filename, the file doesn't exist yet)
   * @throws TSCFileCreateException if the requested filename already exists or if this method exceeded the
   *         maximum number of tries in finding an unused filename in the given folder
   */
  private static IFile findNewFilename(String requestedFilename, IFolder folder, boolean findAlternative)
    throws TSCFileCreateException {
    IFile reqFile;
    IFolder folderWithSameName;
    String suffixedFilename;
    IFile suffixedFile;
    // maximum number of tries to find a non-existing filename
    final int MAX_TRIES = 1024;
    int nbr;

    // if no specific filename was requested a default one is used
    if (requestedFilename == null || requestedFilename.length() == 0) {
      requestedFilename =
        TSC_FILENAME_PREFIX + TSC_FILENAME_SEPERATOR + TSC_FILENAME_DATE_SUFFIX.format(new Date())
          + TSC_FILENAME_EXTENSION;
    }

    reqFile = folder.getFile(requestedFilename);
    folderWithSameName = folder.getFolder(reqFile.getName());

    if (findAlternative && (reqFile.exists() || folderWithSameName.exists())) {
      nbr = 1;
      do {
        suffixedFilename = reqFile.getName() + TSC_FILENAME_SEPERATOR + nbr++;
        suffixedFile = folder.getFile(suffixedFilename);
        folderWithSameName = folder.getFolder(suffixedFilename);
      } while ((suffixedFile.exists() || folderWithSameName.exists()) && nbr < MAX_TRIES);

      if (suffixedFile.exists() || folderWithSameName.exists()) {
        throw new TSCFileCreateException("exceeded maximum number of tries to" + //$NON-NLS-1$
          " find a unique filename"); //$NON-NLS-1$
      }

      reqFile = suffixedFile;
    } else if (!findAlternative && (reqFile.exists() || folderWithSameName.exists())) {
      throw new TSCFileCreateException("filename already in use"); //$NON-NLS-1$
    }
    return reqFile;
  }

  /*
   * methods to load test session containers
   */

  /**
   * Loads a known test session container.
   * 
   * @param tscInfo the TSContainerInfo-representation of the test session container to load
   * @param monitor a progress monitor or null if progress reporting is not desired
   * @return the loaded TestSessionContainer
   * @throws FileLoadException if the TestSessionContainer can't be read from the contents of
   *         the file
   */
  static TestSessionContainer load(TSContainerInfo tscInfo, IProgressMonitor monitor)
    throws FileLoadException {
    monitor = (monitor != null) ? monitor : new NullProgressMonitor();
    // may throw FileLoadException
    return TSContainerStorage.load(tscInfo.getFile(), true, monitor);
  }

  /**
   * Loads the TestSessionContainer from the given file.
   * 
   * @param file the file which contains the test session container to load
   * @param fully if true fully loads the test session container, else only loads some infos
   *        about the test session container (see {@link TestSessionContainer#loadInfoOnly(
   *        org.codecover.model.extensions.PluginManager, Logger, MASTBuilder, String)} for details)
   * @param monitor a progress monitor or null if progress reporting is not desired
   * @return the TestSessionContainer of the given file or null if the given
   *         file doesn't exist.
   * @throws FileLoadException if the TestSessionContainer can't be read from the contents of
   *         the file
   */
  static TestSessionContainer load(final IFile file, final boolean fully, IProgressMonitor monitor)
    throws FileLoadException {
    Logger logger = CodeCoverPlugin.getDefault().getLogger();
    MASTBuilder builder = new MASTBuilder(logger);
    TestSessionContainer tsc = null;
    monitor = (monitor != null) ? monitor : new NullProgressMonitor();

    try {
      monitor.beginTask(String.format(MONITOR_LOADING_TEST_SESSION_CONTAINER_FROM, file.getName()), 2);
      monitor.worked(1);
      if (fully) {
        // may throw FileLoadException
        tsc =
          TestSessionContainer.load(
            CodeCoverPlugin.getDefault().getEclipsePluginManager().getPluginManager(), logger, builder, file
              .getLocation().toOSString());
      } else {
        // may throw FileLoadException
        tsc =
          TestSessionContainer.loadInfoOnly(CodeCoverPlugin.getDefault().getEclipsePluginManager()
            .getPluginManager(), logger, builder, file.getLocation().toOSString());
      }
      monitor.worked(1);
    } finally {
      monitor.done();
    }

    return tsc;
  }

  /**
   * Fetches all files of test session containers of all open projects which are currently in the workspace.
   * It is guaranteed that the returned list only contains files (IFiles), however these
   * files don't have to contain test session containers.
   * 
   * @return all files of test session containers of all open projects which are currently in the workspace.
   */
  static List fetchTSCFiles() {
    return TSContainerStorage.fetchTSCFiles(ResourcesPlugin.getWorkspace().getRoot().getProjects());
  }

  /**
   * Fetches all files of test session containers of the given projects. It is guaranteed that the returned
   * list only contains files (IFiles), however these files don't have to contain test
   * session containers.
   * 
   * @param projects the projects to fetch the files from
   * @return all files of test session containers of the given projects which are currently in the workspace.
   */
  private static List fetchTSCFiles(IProject[] projects) {
    List tscFiles = new LinkedList();
    IFolder codeCoverFolder;
    IResource[] directChildren;
    for (IProject project : projects) {
      if (project.isOpen()) {
        codeCoverFolder = project.getFolder(CodeCoverPlugin.CODECOVER_FOLDER);
        if (codeCoverFolder.exists()) {
          try {
            directChildren = codeCoverFolder.members();
          } catch (CoreException e) {
            // can't happen, just to be safe
            continue;
          }
          for (IResource file : directChildren) {
            if (file instanceof IFile) {
              tscFiles.add((IFile) file);
            }
          }
        }
      }
    }
    return tscFiles;
  }

  /**
   * Deletes files of test session containers.
   * 
   * @param tscInfos the list of test session containers which files are to be deleted
   * @param monitor a progress monitor or null if progress reporting is not desired
   * @throws NullPointerException if the given list of test session containers is null
   * @throws IllegalArgumentException if the given list of test session containers is empty
   * @throws CoreException if deletion of one or more files failed (see {@link IResource#delete(boolean,
   *         IProgressMonitor)} for details); only the CoreException of the last failed
   *         deletion is thrown
   * @throws CancelException if a request to cancel is detected (with the given progress monitor)
   */
  void deleteTSCFiles(List tscInfos, IProgressMonitor monitor)
    throws CoreException, CancelException {
    CoreException lastFileDeleteException;
    final int monitorScale = 1000;
    monitor = (monitor != null) ? monitor : new NullProgressMonitor();
    String cancelDescr = "Canceled deletion of test " + //$NON-NLS-1$
      " session container files.\n" + //$NON-NLS-1$
      "Changes happened so far:\n"; //$NON-NLS-1$

    if (tscInfos == null) {
      throw new NullPointerException("tscInfos mustn't be null"); //$NON-NLS-1$
    }
    if (tscInfos.isEmpty()) {
      throw new IllegalArgumentException("tscInfos mustn't be empty"); //$NON-NLS-1$
    }

    try { // this try only ensures that the monitor is left done()
      monitor.beginTask(MONITOR_DELETING_TSC_FILES, tscInfos.size() * monitorScale);
      if (monitor.isCanceled()) {
        throw new CancelException(cancelDescr + "No files deleted."); //$NON-NLS-1$
      }
      lastFileDeleteException = null;
      for (TSContainerInfo tscToDelete : tscInfos) {
        try {
          tscToDelete.getFile().delete(true, new SubProgressMonitor(monitor, 1 * monitorScale));
        } catch (CoreException e) {
          lastFileDeleteException = e;
        }
        cancelDescr += tscToDelete.getPath().toString() + " deleted\n"; //$NON-NLS-1$
        if (monitor.isCanceled()) {
          throw new CancelException(cancelDescr);
        }
      }
      if (lastFileDeleteException != null) {
        throw lastFileDeleteException;
      }
    } finally {
      monitor.done();
    }
  }

}