# ReportMojo.java

    /*******************************************************************************
     * Copyright (c) 2009, 2013 Mountainminds GmbH & Co. KG and Contributors
     * 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:
     *    Evgeny Mandrikov - initial API and implementation
     *    Kyle Lieber - implementation of CheckMojo
     *
     *******************************************************************************/
    package org.jacoco.maven;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.Reader;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Locale;

    import org.apache.maven.doxia.siterenderer.Renderer;
    import org.apache.maven.plugin.MojoExecutionException;
    import org.apache.maven.project.MavenProject;
    import org.apache.maven.reporting.AbstractMavenReport;
    import org.apache.maven.reporting.MavenReportException;
    import org.jacoco.core.analysis.IBundleCoverage;
    import org.jacoco.core.analysis.ICoverageNode;
    import org.jacoco.core.data.ExecFileLoader;
    import org.jacoco.core.data.ExecutionDataStore;
    import org.jacoco.core.data.SessionInfoStore;
    import org.jacoco.report.FileMultiReportOutput;
    import org.jacoco.report.IReportGroupVisitor;
    import org.jacoco.report.IReportVisitor;
    import org.jacoco.report.ISourceFileLocator;
    import org.jacoco.report.MultiReportVisitor;
    import org.jacoco.report.csv.CSVFormatter;
    import org.jacoco.report.html.HTMLFormatter;
    import org.jacoco.report.xml.XMLFormatter;

    /**
     * Creates a code coverage report for a single project in multiple formats
     * (HTML, XML, and CSV).
     *
     * @phase verify
     * @goal report
     * @requiresProject true
     * @threadSafe
     */
    public class ReportMojo extends AbstractMavenReport {

    	/**
    	 * Output directory for the reports. Note that this parameter is only
    	 * relevant if the goal is run from the command line or from the default
    	 * build lifecycle. If the goal is run indirectly as part of a site
    	 * generation, the output directory configured in the Maven Site Plugin is
    	 * used instead.
    	 *
    	 * @parameter default-value="${project.reporting.outputDirectory}/jacoco"
    	 */
    	private File outputDirectory;

    	/**
    	 * Encoding of the generated reports.
    	 *
    	 * @parameter expression="${project.reporting.outputEncoding}"
    	 *            default-value="UTF-8"
    	 */
    	private String outputEncoding;

    	/**
    	 * Encoding of the source files.
    	 *
    	 * @parameter expression="${project.build.sourceEncoding}"
    	 *            default-value="UTF-8"
    	 */
    	private String sourceEncoding;

    	/**
    	 * File with execution data.
    	 *
    	 * @parameter default-value="${project.build.directory}/jacoco.exec"
    	 */
    	private File dataFile;

    	/**
    	 * A list of class files to include in the report. May use wildcard
    	 * characters (* and ?). When not specified everything will be included.
    	 *
    	 * @parameter
    	 */
    	private List<String> includes;

    	/**
    	 * A list of class files to exclude from the report. May use wildcard
    	 * characters (* and ?). When not specified nothing will be excluded.
    	 *
    	 * @parameter
    	 */
    	private List<String> excludes;

    	/**
    	 * Flag used to suppress execution.
    	 *
    	 * @parameter expression="${jacoco.skip}" default-value="false"
    	 */
    	private boolean skip;

    	/**
    	 * Maven project.
    	 *
    	 * @parameter expression="${project}"
    	 * @readonly
    	 */
    	private MavenProject project;

    	/**
    	 * Doxia Site Renderer.
    	 *
    	 * @component
    	 */
    	private Renderer siteRenderer;

    	private SessionInfoStore sessionInfoStore;

    	private ExecutionDataStore executionDataStore;

    	public String getOutputName() {
    		return "jacoco/index";
    	}

    	public String getName(final Locale locale) {
    		return "JaCoCo";
    	}

    	public String getDescription(final Locale locale) {
    		return "JaCoCo Test Coverage Report.";
    	}

    	@Override
    	public boolean isExternalReport() {
    		return true;
    	}

    	@Override
    	protected String getOutputDirectory() {
    		return outputDirectory.getAbsolutePath();
    	}

    	@Override
    	protected MavenProject getProject() {
    		return project;
    	}

    	@Override
    	protected Renderer getSiteRenderer() {
    		return siteRenderer;
    	}

    	/**
    	 * Returns the list of class files to include in the report.
    	 *
    	 * @return class files to include, may contain wildcard characters
    	 */
    	protected List<String> getIncludes() {
    		return includes;
    	}

    	/**
    	 * Returns the list of class files to exclude from the report.
    	 *
    	 * @return class files to exclude, may contain wildcard characters
    	 */
    	protected List<String> getExcludes() {
    		return excludes;
    	}

    	@Override
    	public void setReportOutputDirectory(final File reportOutputDirectory) {
    		if (reportOutputDirectory != null
    				&& !reportOutputDirectory.getAbsolutePath().endsWith("jacoco")) {
    			outputDirectory = new File(reportOutputDirectory, "jacoco");
    		} else {
    			outputDirectory = reportOutputDirectory;
    		}
    	}

    	@Override
    	public boolean canGenerateReport() {
    		if ("pom".equals(project.getPackaging())) {
    			getLog().info(
    					"Skipping JaCoCo for project with packaging type 'pom'");
    			return false;
    		}
    		if (skip) {
    			getLog().info("Skipping JaCoCo execution");
    			return false;
    		}
    		if (!dataFile.exists()) {
    			getLog().info(
    					"Skipping JaCoCo execution due to missing execution data file");
    			return false;
    		}
    		return true;
    	}

    	/**
    	 * This method is called when the report generation is invoked directly as a
    	 * standalone Mojo.
    	 */
    	@Override
    	public void execute() throws MojoExecutionException {
    		if (!canGenerateReport()) {
    			return;
    		}
    		try {
    			executeReport(Locale.getDefault());
    		} catch (final MavenReportException e) {
    			throw new MojoExecutionException("An error has occurred in "
    					+ getName(Locale.ENGLISH) + " report generation.", e);
    		}
    	}

    	@Override
    	protected void executeReport(final Locale locale)
    			throws MavenReportException {
    		loadExecutionData();
    		try {
    			final IReportVisitor visitor = createVisitor(locale);
    			visitor.visitInfo(sessionInfoStore.getInfos(),
    					executionDataStore.getContents());
    			createReport(visitor);
    			visitor.visitEnd();
    		} catch (final IOException e) {
    			throw new MavenReportException("Error while creating report: "
    					+ e.getMessage(), e);
    		}
    	}

    	private void loadExecutionData() throws MavenReportException {
    		final ExecFileLoader loader = new ExecFileLoader();
    		try {
    			loader.load(dataFile);
    		} catch (final IOException e) {
    			throw new MavenReportException(
    					"Unable to read execution data file " + dataFile + ": "
    							+ e.getMessage(), e);
    		}
    		sessionInfoStore = loader.getSessionInfoStore();
    		executionDataStore = loader.getExecutionDataStore();
    	}

    	private void createReport(final IReportGroupVisitor visitor)
    			throws IOException {
    		final FileFilter fileFilter = new FileFilter(this.getIncludes(),
    				this.getExcludes());
    		final BundleCreator creator = new BundleCreator(this.getProject(),
    				fileFilter);
    		final IBundleCoverage bundle = creator.createBundle(executionDataStore);

    		final SourceFileCollection locator = new SourceFileCollection(
    				getCompileSourceRoots(), sourceEncoding);
    		checkForMissingDebugInformation(bundle);
    		visitor.visitBundle(bundle, locator);
    	}

    	private void checkForMissingDebugInformation(final ICoverageNode node) {
    		if (node.getClassCounter().getTotalCount() > 0
    				&& node.getLineCounter().getTotalCount() == 0) {
    			getLog().warn(
    					"To enable source code annotation class files have to be compiled with debug information.");
    		}
    	}

    	private IReportVisitor createVisitor(final Locale locale)
    			throws IOException {
    		final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>();

    		outputDirectory.mkdirs();

    		final XMLFormatter xmlFormatter = new XMLFormatter();
    		xmlFormatter.setOutputEncoding(outputEncoding);
    		visitors.add(xmlFormatter.createVisitor(new FileOutputStream(new File(
    				outputDirectory, "jacoco.xml"))));

    		final CSVFormatter csvFormatter = new CSVFormatter();
    		csvFormatter.setOutputEncoding(outputEncoding);
    		visitors.add(csvFormatter.createVisitor(new FileOutputStream(new File(
    				outputDirectory, "jacoco.csv"))));

    		final HTMLFormatter htmlFormatter = new HTMLFormatter();
    		htmlFormatter.setOutputEncoding(outputEncoding);
    		htmlFormatter.setLocale(locale);
    		visitors.add(htmlFormatter.createVisitor(new FileMultiReportOutput(
    				outputDirectory)));

    		return new MultiReportVisitor(visitors);
    	}

    	private static class SourceFileCollection implements ISourceFileLocator {

    		private final List<File> sourceRoots;
    		private final String encoding;

    		public SourceFileCollection(final List<File> sourceRoots,
    				final String encoding) {
    			this.sourceRoots = sourceRoots;
    			this.encoding = encoding;
    		}

    		public Reader getSourceFile(final String packageName,
    				final String fileName) throws IOException {
    			final String r;
    			if (packageName.length() > 0) {
    				r = packageName + '/' + fileName;
    			} else {
    				r = fileName;
    			}
    			for (final File sourceRoot : sourceRoots) {
    				final File file = new File(sourceRoot, r);
    				if (file.exists() && file.isFile()) {
    					return new InputStreamReader(new FileInputStream(file),
    							encoding);
    				}
    			}
    			return null;
    		}

    		public int getTabWidth() {
    			return 4;
    		}
    	}

    	private File resolvePath(final String path) {
    		File file = new File(path);
    		if (!file.isAbsolute()) {
    			file = new File(getProject().getBasedir(), path);
    		}
    		return file;
    	}

    	private List<File> getCompileSourceRoots() {
    		final List<File> result = new ArrayList<File>();
    		for (final Object path : getProject().getCompileSourceRoots()) {
    			result.add(resolvePath((String) path));
    		}
    		return result;
    	}
    }