org.codecover.eclipse.builder.CodeCoverCompilationParticipant.java Source code

Java tutorial

Introduction

Here is the source code for org.codecover.eclipse.builder.CodeCoverCompilationParticipant.java

Source

/******************************************************************************
 * Copyright (c) 2007 Stefan Franke, Robert Hanussek, Benjamin Keil,          *
 *                    Steffen Kie, Johannes Langauf,                         *
 *                    Christoph Marian Mller, 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.builder;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;

import org.codecover.eclipse.CodeCoverPlugin;
import org.codecover.eclipse.InstrumentableItemsManager;
import org.codecover.eclipse.tscmanager.ActiveTSContainerInfo;
import org.codecover.eclipse.tscmanager.TSContainerInfo;
import org.codecover.eclipse.tscmanager.TSContainerManager;
import org.codecover.eclipse.tscmanager.exceptions.CancelException;
import org.codecover.eclipse.tscmanager.exceptions.TSCFileCreateException;
import org.codecover.eclipse.utils.EclipseMASTLinkage;
import org.codecover.instrumentation.DefaultInstrumenterFactory;
import org.codecover.instrumentation.Instrumenter;
import org.codecover.instrumentation.InstrumenterDescriptor;
import org.codecover.instrumentation.InstrumenterFactory;
import org.codecover.instrumentation.exceptions.FactoryMisconfigurationException;
import org.codecover.instrumentation.exceptions.InstrumentationException;
import org.codecover.instrumentation.exceptions.InstrumentationIOException;
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.mast.HierarchyLevel;
import org.codecover.model.mast.Location;
import org.codecover.model.utils.Logger;
import org.codecover.model.utils.criteria.Criterion;
import org.codecover.model.utils.file.FileTool;
import org.codecover.model.utils.file.SourceTargetContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.BuildContext;
import org.eclipse.jdt.core.compiler.CompilationParticipant;

/**
 * The compilation participants are called by eclipse's java builder to allow
 * plugins to participate on a build. The buildStarting method starts the
 * instrumentation in a .codecover folder in the project folder.<br>
 * <b>Charset:</b> We get the charset of each code file but we use 
 * {@link #DEFAULT_CHARSET_FOR_COMPILING} as the target charset, because all
 * files have to be compiled with the same charset.
 *
 * @author Stefan Franke, Tilmann Scheller, Christoph Mller
 * @version 1.0 ($Id$)
 * 
 */
public class CodeCoverCompilationParticipant extends CompilationParticipant {

    private static final Charset DEFAULT_CHARSET_FOR_COMPILING = Charset.forName("UTF-8"); //$NON-NLS-1$

    private static final boolean SEARCH_IN_ALL_TSC = false;

    private final Logger eclipseLogger = CodeCoverPlugin.getDefault().getLogger();

    @Override
    public int aboutToBuild(IJavaProject project) {
        // until the instrumenter can handle incremental instrumentation we need to do full builds
        return CompilationParticipant.NEEDS_FULL_BUILD;
    }

    /**
     * This method is called for each project separately.
     */
    @Override
    public void buildStarting(BuildContext[] files, boolean isBatch) {
        if (files.length == 0) {
            return;
        }

        final IProject iProject = files[0].getFile().getProject();
        final IPath projectFullPath = iProject.getFullPath();
        final IPath projectLocation = iProject.getLocation();
        final String projectFolderLocation = iProject.getLocation().toOSString();
        final Queue<SourceTargetContainer> sourceTargetContainers = new LinkedList<SourceTargetContainer>();
        final String instrumentedFolderLocation = CodeCoverPlugin.getDefault()
                .getPathToInstrumentedSources(iProject).toOSString();

        // try to get all source folders
        final Queue<String> possibleSourceFolders = new LinkedList<String>();
        try {
            final IJavaProject javaProject = JavaCore.create(iProject);
            for (IPackageFragmentRoot thisRoot : javaProject.getAllPackageFragmentRoots()) {
                IResource resource = thisRoot.getCorrespondingResource();
                if (resource != null) {
                    possibleSourceFolders.add(resource.getLocation().toOSString());
                }
            }
        } catch (JavaModelException e) {
            eclipseLogger.fatal("JavaModelException in CompilationParticipant", e); //$NON-NLS-1$
            return;
        }

        InstrumentableItemsManager instrumentableItemsManager = CodeCoverPlugin.getDefault()
                .getInstrumentableItemsManager();
        TSContainerManager tscManager = CodeCoverPlugin.getDefault().getTSContainerManager();

        // //////////////////////////////////////////////////////////////////////
        // CREATE THE SOURCE TARGET CONTAINERS AND COPY THE UNINSTRUMENTED
        // FILES TO THE INSTRUMENTEDFOLDERLOCATION
        // //////////////////////////////////////////////////////////////////////
        fillSourceTargetContainers(files, possibleSourceFolders, sourceTargetContainers, instrumentedFolderLocation,
                eclipseLogger, instrumentableItemsManager);

        // //////////////////////////////////////////////////////////////////////
        // SEARCH IN ALL TSC OF THIS PROJECT IF A TSC CAN BE REUSED
        // //////////////////////////////////////////////////////////////////////

        TestSessionContainer tsc;
        if (SEARCH_IN_ALL_TSC) {
            tsc = searchUseableTSC(iProject, files, instrumentableItemsManager, tscManager);
        } else {
            tsc = getUseableTSC(files, instrumentableItemsManager, tscManager);
        }

        // //////////////////////////////////////////////////////////////////////
        // PREPARE INSTRUMENTATION
        // //////////////////////////////////////////////////////////////////////
        InstrumenterDescriptor descriptor = new org.codecover.instrumentation.java15.InstrumenterDescriptor();
        if (descriptor == null) {
            eclipseLogger.fatal("NullPointerException in CompilationParticipant"); //$NON-NLS-1$
        }

        // check whether TSC's criteria match
        // with the selected criteria of the project
        if (tsc != null) {
            Set<Criterion> tscCriteria = tsc.getCriteria();

            for (Criterion criterion : tscCriteria) {
                if (!CodeCoverPlugin.getCriterionSelectedState(iProject, criterion)) {
                    // the TSC uses a criterion which is not selected for the project
                    // therefore it can't be used
                    tsc = null;
                }
            }

            // all selected criteria must be active for the TSC
            for (Criterion criterion : descriptor.getSupportedCriteria()) {
                if (CodeCoverPlugin.getCriterionSelectedState(iProject, criterion)) {
                    if (!tscCriteria.contains(criterion)) {
                        // the TSC doesn't use a criterion which is selected
                        // for the project, this means we can't use the TSC
                        tsc = null;
                    }
                }
            }
        }

        eclipseLogger.debug("can reuse TSC: " + (tsc != null ? tsc : "no")); //$NON-NLS-1$ //$NON-NLS-2$

        InstrumenterFactory factory = new DefaultInstrumenterFactory();
        factory.setDescriptor(descriptor);

        // only instrument with the selected criteria
        for (Criterion criterion : descriptor.getSupportedCriteria()) {
            if (CodeCoverPlugin.getCriterionSelectedState(iProject, criterion)) {
                factory.addCriterion(criterion);
            }
        }

        factory.setCharset(DEFAULT_CHARSET_FOR_COMPILING);

        Instrumenter instrumenter = null;
        try {
            instrumenter = factory.getInstrumenter();
        } catch (FactoryMisconfigurationException e) {
            eclipseLogger.fatal("FactoryMisconfigurationException in CompilationParticipant"); //$NON-NLS-1$
        }

        // //////////////////////////////////////////////////////////////////////
        // INSTRUMENT
        // //////////////////////////////////////////////////////////////////////
        File rootFolder = new File(projectFolderLocation);
        File targetFolder = new File(instrumentedFolderLocation);
        MASTBuilder builder = new MASTBuilder(eclipseLogger);
        Map<String, Object> instrumenterDirectives = descriptor.getDefaultDirectiveValues();
        CodeCoverPlugin plugin = CodeCoverPlugin.getDefault();
        eclipseLogger.debug("Plugin: " + plugin);
        IPath coverageLogPath = CodeCoverPlugin.getDefault().getPathToCoverageLogs(iProject);
        coverageLogPath = coverageLogPath.append("coverage-log-file.clf"); //$NON-NLS-1$

        instrumenterDirectives.put(
                org.codecover.instrumentation.java15.InstrumenterDescriptor.CoverageLogPathDirective.KEY,
                coverageLogPath.toOSString());

        if (tsc != null) {
            // we can reuse the TSC
            instrumenterDirectives.put(org.codecover.instrumentation.UUIDDirective.KEY, tsc.getId());
        }

        TestSessionContainer testSessionContainer;
        try {
            testSessionContainer = instrumenter.instrument(rootFolder, targetFolder, sourceTargetContainers,
                    builder, instrumenterDirectives);
        } catch (InstrumentationIOException e) {
            eclipseLogger.fatal("InstrumentationIOException in CompilationParticipant", e); //$NON-NLS-1$
            return;
        } catch (InstrumentationException e) {
            eclipseLogger.fatal("InstrumentationException in CompilationParticipant", e); //$NON-NLS-1$
            return;
        }

        // //////////////////////////////////////////////////////////////////////
        // SAVE TSC
        // //////////////////////////////////////////////////////////////////////
        if (tsc == null) {
            // we have to create a new TSC
            try {
                tscManager.addTestSessionContainer(testSessionContainer, iProject, false, null, null);
            } catch (FileSaveException e) {
                eclipseLogger.fatal("FileSaveException in CompilationParticipant", e); //$NON-NLS-1$
            } catch (TSCFileCreateException e) {
                eclipseLogger.fatal("CoreException in CompilationParticipant", e); //$NON-NLS-1$
            } catch (FileLoadException e) {
                eclipseLogger.fatal("CoreException in CompilationParticipant", e); //$NON-NLS-1$
            } catch (InvocationTargetException e) {
                // can't happen because we didn't pass a runnable
                eclipseLogger.warning("InvocationTargetException in CompilationParticipant", e); //$NON-NLS-1$
            } catch (CancelException e) {
                eclipseLogger.warning("User canceled writing of" + //$NON-NLS-1$
                        " new test session container in" + //$NON-NLS-1$
                        " CompilationParticipant"); //$NON-NLS-1$
            }
        }

        // TODO handle compilation errors
        IJavaProject javaProject = JavaCore.create(iProject);

        // set up classpath
        StringBuilder runCommand = new StringBuilder(1024);
        IClasspathEntry[] cpEntries;
        try {
            cpEntries = javaProject.getResolvedClasspath(true);
        } catch (JavaModelException e) {
            eclipseLogger.fatal("JavaModelException in CompilationParticipant", e); //$NON-NLS-1$
            return;
        }

        for (int i = 0; i < cpEntries.length; i++) {
            IClasspathEntry thisEntry = cpEntries[i];
            if (thisEntry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
                if (runCommand.length() == 0) {
                    // this is the first entry -> create the class path option
                    runCommand.append("-cp "); //$NON-NLS-1$
                } else {
                    // this is not the first -> we need a separator
                    runCommand.append(File.pathSeparatorChar);
                }
                runCommand.append("\""); //$NON-NLS-1$
                IPath itsIPath = thisEntry.getPath();
                if (projectFullPath.isPrefixOf(itsIPath)) {
                    itsIPath = itsIPath.removeFirstSegments(1);
                    itsIPath = projectLocation.append(itsIPath);
                }
                runCommand.append(itsIPath.toOSString());
                runCommand.append("\""); //$NON-NLS-1$
            }
        }

        // check java version related options
        String targetVersion = javaProject.getOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, true);
        runCommand.append(" -target "); //$NON-NLS-1$
        runCommand.append(targetVersion);
        String sourceVersion = javaProject.getOption(JavaCore.COMPILER_SOURCE, true);
        runCommand.append(" -source "); //$NON-NLS-1$
        runCommand.append(sourceVersion);

        // no warnings
        runCommand.append(" -nowarn"); //$NON-NLS-1$

        // use the default charset for the encoding
        // all files have been instrumented or copied using this charset
        runCommand.append(" -encoding "); //$NON-NLS-1$
        runCommand.append(DEFAULT_CHARSET_FOR_COMPILING.toString());

        // the directory to compile
        // put the path in "", because the commandline tokenizes this path
        runCommand.append(" \""); //$NON-NLS-1$
        runCommand.append(instrumentedFolderLocation);
        runCommand.append("\""); //$NON-NLS-1$

        eclipseLogger.debug("I run this compile command now:\n" + runCommand); //$NON-NLS-1$
        StringWriter out = new StringWriter();
        StringWriter err = new StringWriter();
        boolean result;
        result = org.eclipse.jdt.internal.compiler.batch.Main.compile(runCommand.toString(), new PrintWriter(out),
                new PrintWriter(err));

        eclipseLogger.debug("ECJ Output: " + out.toString()); //$NON-NLS-1$
        eclipseLogger.debug("ECJ Error Output: " + err.toString()); //$NON-NLS-1$

        if (!result) {
            eclipseLogger.fatal("An error occured when trying to compile the instrumented sources."); //$NON-NLS-1$
        }

        super.buildStarting(files, isBatch);
    }

    private void fillSourceTargetContainers(final BuildContext[] files, final Queue<String> possibleSourceFolders,
            final Collection<SourceTargetContainer> sourceTargetContainers, final String instrumentedFolderLocation,
            final Logger eclipseLogger, final InstrumentableItemsManager instrumentableItemsManager) {

        for (BuildContext buildContext : files) {
            final IFile iFile = buildContext.getFile();
            final String sourceFileLocation = iFile.getLocation().makeAbsolute().toOSString();

            // try to find the related source folder (IPackageFragmentRoot)
            // we do not know, which source folder is the correct one, because
            // you can have source folders in source folders, so we get the one
            // with the longest fit to sourceFileLoacation
            String sourceFolderWithBestFit = null;
            for (String thisSourceFolder : possibleSourceFolders) {
                if (sourceFileLocation.startsWith(thisSourceFolder)) {
                    // this might be the related source folder
                    if (sourceFolderWithBestFit == null
                            || thisSourceFolder.length() > sourceFolderWithBestFit.length()) {
                        sourceFolderWithBestFit = thisSourceFolder;
                    }
                }
            }
            if (sourceFolderWithBestFit == null) {
                eclipseLogger.fatal("We try to instrument a file, that is not under a source folder"); //$NON-NLS-1$
            }

            // we get the relative path under the source folder and add it to
            // the target of the instrumentation folder
            String instrumentFileLocation = instrumentedFolderLocation
                    + sourceFileLocation.substring(sourceFolderWithBestFit.length());

            File sourceFile = new File(sourceFileLocation);
            File instrumentFile = new File(instrumentFileLocation);

            // try to get the charset of this file
            Charset charsetOfFile = null;
            try {
                charsetOfFile = Charset.forName(iFile.getCharset());
            } catch (CoreException e) {
                e.printStackTrace();
                charsetOfFile = DEFAULT_CHARSET_FOR_COMPILING;
            }
            if (instrumentableItemsManager.containsIPath(iFile.getFullPath())) {
                // has to be instrumented!
                SourceTargetContainer sourceTargetContainer = new SourceTargetContainer(sourceFile, instrumentFile,
                        charsetOfFile);
                sourceTargetContainers.add(sourceTargetContainer);
            } else {
                // not selected for instrumentation ->
                // copy it by using the defaultCharset for the target
                try {
                    FileTool.copy(sourceFile, charsetOfFile, instrumentFile, DEFAULT_CHARSET_FOR_COMPILING);
                } catch (IOException e) {
                    eclipseLogger.fatal("IOException in CompilationParticipant", e); //$NON-NLS-1$
                }
            }
        }
    }

    private TestSessionContainer searchUseableTSC(IProject project, BuildContext[] files,
            InstrumentableItemsManager instrumentableItemsManager, TSContainerManager tscManager) {
        List<TSContainerInfo> allTSContainerInfos = tscManager.getTestSessionContainers(project);
        if (allTSContainerInfos.isEmpty()) {
            // no TSC found for this project
            return null;
        }

        // sort the TSCInfo by their date descending
        TreeMap<Date, TSContainerInfo> descendingTSC = new TreeMap<Date, TSContainerInfo>(new Comparator<Date>() {
            public int compare(Date date1, Date date2) {
                return -1 * date1.compareTo(date2);
            }
        });
        for (TSContainerInfo thisTSCInfo : allTSContainerInfos) {
            descendingTSC.put(thisTSCInfo.getDate(), thisTSCInfo);
        }

        // now, we open all TestSessionContainers to check, whether it is 
        // the one, we can use

        // save the current TSC
        ActiveTSContainerInfo activeTSCBefore = tscManager.getActiveTSContainer();
        TSContainerInfo activeInfoBefore = null;
        if (activeTSCBefore != null) {
            activeInfoBefore = activeTSCBefore.getTSContainerInfo();
        }

        TestSessionContainer tscToUse = null;
        tscLoop: for (Entry<Date, TSContainerInfo> thisEntry : descendingTSC.entrySet()) {
            TSContainerInfo thisInfo = thisEntry.getValue();
            tscToUse = getUseableTSC(thisInfo, files, tscManager, instrumentableItemsManager);
            if (tscToUse != null) {
                // found a useable tsc
                break tscLoop;
            }
        }

        // restore the old TSC
        ActiveTSContainerInfo activeTSCNow = tscManager.getActiveTSContainer();
        TSContainerInfo activeInfoNow = null;
        if (activeTSCNow != null) {
            activeInfoNow = activeTSCNow.getTSContainerInfo();
        }
        if ((activeInfoBefore == null && activeInfoNow != null)
                || (activeInfoBefore != null && activeInfoNow == null)
                || (activeInfoBefore != null && activeInfoNow != null && !activeInfoBefore.equals(activeInfoNow))) {
            // we have to restore the TSC before

            try {
                tscManager.setActiveTSContainer(activeInfoBefore, null, null);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // nothing found
        return tscToUse;
    }

    /**
     * It is checked, whether the {@link TestSessionContainer#getCode()} of
     * the {@link ActiveTSContainerInfo} is equal to the <code>files</code>.<br>
     * If yes, the {@link TestSessionContainer} is returned&mdash;<code>null</code> else.
     */
    private TestSessionContainer getUseableTSC(BuildContext[] files,
            InstrumentableItemsManager instrumentableItemsManager, TSContainerManager tscManager) {
        ActiveTSContainerInfo activeTSC = tscManager.getActiveTSContainer();
        if (activeTSC != null && activeTSC.getTSContainerInfo() != null) {
            return getUseableTSC(activeTSC.getTSContainerInfo(), files, tscManager, instrumentableItemsManager);
        } else {
            return null;
        }
    }

    /**
     * If the given {@link TSContainerInfo} is not the {@link ActiveTSContainerInfo}
     * than this it is opened. Afterwards it is checked, whether its 
     * {@link TestSessionContainer#getCode()} is equal to the <code>files</code>.<br>
     * If yes, the {@link TestSessionContainer} is returned&mdash;<code>null</code> else.
     * @param tscManager TODO
     */
    private TestSessionContainer getUseableTSC(TSContainerInfo thisInfo, BuildContext[] files,
            TSContainerManager tscManager, InstrumentableItemsManager instrumentableItemsManager) {
        ActiveTSContainerInfo activeTSC = tscManager.getActiveTSContainer();
        TSContainerInfo activeInfo = null;
        if (activeTSC != null) {
            activeInfo = activeTSC.getTSContainerInfo();
        }

        if (activeInfo == null || !activeInfo.equals(thisInfo)) {
            // have to open TSC first
            try {
                tscManager.setActiveTSContainer(thisInfo, null, null);
            } catch (Exception e) {
                return null;
            }
        }

        activeTSC = tscManager.getActiveTSContainer();
        TestSessionContainer tsc = null;
        activeInfo = null;
        if (activeTSC != null) {
            tsc = activeTSC.getTestSessionContainer();
            activeInfo = activeTSC.getTSContainerInfo();
        }
        if (activeInfo == null || tsc == null || !activeInfo.equals(thisInfo)) {
            // opening went wrong
            return null;
        }

        // assert: we have thisInfo opened

        eclipseLogger.debug("check TSC " + tsc); //$NON-NLS-1$
        if (isCodeUnchanged(tsc.getCode(), files, instrumentableItemsManager)) {
            return tsc;
        } else {
            return null;
        }
    }

    private boolean isCodeUnchanged(HierarchyLevel code, BuildContext[] files,
            InstrumentableItemsManager instrumentableItemsManager) {
        boolean result = true;
        // check whether we need a new TSC
        // this is done by checking every file
        for (BuildContext buildContext : files) {
            final IFile file = buildContext.getFile();
            final IJavaElement compilationUnit = JavaCore.create(file);

            // find the source file, which contains the public class
            // with the same name the "file" has got
            final HierarchyLevel codeFile = EclipseMASTLinkage.findSource(code, compilationUnit);

            final boolean instrument = instrumentableItemsManager.containsIPath(file.getFullPath());
            final boolean fileInTSC = codeFile != null;

            if (instrument && fileInTSC) {
                String mastText;
                Location mastLocation;

                // get the source file of the location
                mastLocation = codeFile.getLocation().getLocations().get(0);
                mastText = mastLocation.getFile().getContent();
                // FIXME deal with unsynced files
                try {
                    InputStream inStream = file.getContents();
                    Charset charsetOfFile = Charset.forName(file.getCharset());
                    String contentOfFile = FileTool.getContentFromStream(inStream, charsetOfFile);

                    if (!mastText.equals(contentOfFile)) {
                        // code file is changed to the MAST
                        eclipseLogger.debug(file.getName() + " has changed (break)"); //$NON-NLS-1$
                        result = false;
                        break;
                    }
                } catch (CoreException e) {
                    e.printStackTrace();
                    return false;
                } catch (IOException e) {
                    e.printStackTrace();
                    return false;
                }
            } else if (instrument && !fileInTSC) {
                // codeFile == null -> code file not in MAST
                eclipseLogger.debug("was not found in the MAST (break)"); //$NON-NLS-1$
                result = false;
                break;
            } else if (!instrument && fileInTSC) {
                // checking of files which are not to be instrumented
                // since the TSC can only contain files which are to be
                // instrumented, we need a new TSC in case it contains
                // entries for files which are not to be instrumented
                // (because they obviously will never be covered and
                // therefore distort the calculated coverage metrics)

                // file which is not to be instrumented is part of the TSC
                result = false;
                break;
            }
        }

        // every check was successful
        return result;
    }

    @Override
    public void cleanStarting(IJavaProject project) {
        eclipseLogger.debug("cleaning"); //$NON-NLS-1$
        File filename = CodeCoverPlugin.getDefault().getPathToInstrumentedSources(project.getProject()).toFile();
        deleteDir(filename);
    }

    private boolean deleteDir(File dir) {
        // to see if this directory is actually a symbolic link to a directory,
        // we want to get its canonical path - that is, we follow the link to
        // the file it's actually linked to
        File candir;
        try {
            candir = dir.getCanonicalFile();
        } catch (IOException e) {
            return false;
        }

        // a symbolic link has a different canonical path than its actual path,
        // unless it's a link to itself
        if (!candir.equals(dir.getAbsoluteFile())) {
            // this file is a symbolic link, and there's no reason for us to
            // follow it, because then we might be deleting something outside of
            // the directory we were told to delete
            return false;
        }

        // now we go through all of the files and subdirectories in the
        // directory and delete them one by one
        File[] files = candir.listFiles();
        if (files != null) {
            for (int i = 0; i < files.length; i++) {
                File file = files[i];

                // in case this directory is actually a symbolic link, or it's
                // empty, we want to try to delete the link before we try
                // anything
                boolean deleted = file.delete();
                if (!deleted) {
                    // deleting the file failed, so maybe it's a non-empty
                    // directory
                    if (file.isDirectory())
                        deleteDir(file);

                    // otherwise, there's nothing else we can do
                }
            }
        }

        // now that we tried to clear the directory out, we can try to delete it
        // again
        return dir.delete();
    }

    @Override
    public boolean isActive(IJavaProject project) {
        // only handle projects which are actually enabled for use with CodeCover
        return CodeCoverPlugin.isCodeCoverActivated(project.getProject());
    }

}