package com.atlassian.maven.plugin.clover;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
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.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.PropertyHelper;
import org.codehaus.plexus.resource.ResourceManager;
import com.atlassian.maven.plugin.clover.internal.AbstractCloverMojo;
import com.atlassian.maven.plugin.clover.internal.AntPropertyHelper;
import com.atlassian.maven.plugin.clover.internal.CloverConfiguration;
import com.atlassian.maven.plugin.clover.internal.ConfigUtil;
import com.cenqua.clover.cfg.Interval;
/**
* Generate a <a href="http://atlassian.com/software/clover">Clover</a> report from existing Clover databases. The generated report
* is an external report generated by Clover itself. If the project generating the report is a top level project and
* if the <code>aggregate</code> configuration element is set to true then an aggregated report will also be created.
* <p/>
* <p>Note: This report mojo should be an @aggregator and the <code>clover:aggregate</code> mojo shouldn't exist. This
* is a limitation of the site plugin which doesn't support @aggregator reports...</p>
*
* @goal clover
*/
public class CloverReportMojo extends AbstractMavenReport implements CloverConfiguration {
// TODO: Need some way to share config elements and code between report mojos and main build mojos.
// See http://jira.codehaus.org/browse/MNG-1886
/**
* Use a custom report descriptor for generating your Clover Reports.
* The format for the configuration file is identical to an Ant build file which uses the <clover-report/>
* task. For a complete reference, please consult the
* <a href="http://confluence.atlassian.com/x/34dEB">clover-report documentation</a>
*
* @parameter expression="${maven.clover.reportDescriptor}"
*/
private File reportDescriptor;
/**
* If set to true, the clover-report configuration file will be resolved as a versioned artifact by looking for it
* in your configured maven repositories - both remote and local.
*
* @parameter expression="${maven.clover.resolveReportDescriptor}" default-value="false"
*/
private boolean resolveReportDescriptor;
/**
* The component that is used to resolve additional artifacts required.
*
* @component
*/
protected ArtifactResolver artifactResolver;
/**
* Remote repositories used for the project.
*
* @parameter expression="${project.remoteArtifactRepositories}"
*/
protected List repositories;
/**
* The component used for creating artifact instances.
*
* @component
*/
protected ArtifactFactory artifactFactory;
/**
* The local repository.
*
* @parameter expression="${localRepository}"
*/
protected ArtifactRepository localRepository;
/**
* The location of the <a href="http://confluence.atlassian.com/x/EIBOB">Clover database</a>.
*
* @parameter expression="${maven.clover.cloverDatabase}"
*/
private String cloverDatabase;
/**
* If true, then a single database will be saved for the entire project, in the target directory of the execution
* root.
* If a custom location for the cloverDatabase is specified, this flag is ignored.
* @parameter expression="${maven.clover.singleCloverDatabase}" default-value="false"
*/
protected boolean singleCloverDatabase;
/**
* The location of the merged clover database to create when running a report in a multimodule build.
*
* @parameter expression="${maven.clover.cloverMergeDatabase}" default-value="${project.build.directory}/clover/cloverMerge.db"
* @required
*/
private String cloverMergeDatabase;
/**
* The directory where the Clover report will be generated.
*
* @parameter expression="${maven.clover.outputDirectory}" default-value="${project.reporting.outputDirectory}/clover"
* @required
*/
private File outputDirectory;
/**
* The location where historical Clover data will be saved.
* <p/>
* <p>Note: It's recommended to modify the location of this directory so that it points to a more permanent
* location as the <code>${project.build.directory}</code> directory is erased when the project is cleaned.</p>
*
* @parameter expression="${maven.clover.historyDir}" default-value="${project.build.directory}/clover/history"
* @required
*/
private String historyDir;
/**
* When the Clover Flush Policy is set to "interval" or threaded this value is the minimum
* period between flush operations (in milliseconds).
*
* @parameter expression="${maven.clover.flushInterval}" default-value="500"
*/
private int flushInterval;
/**
* If true we'll wait 2*flushInterval to ensure coverage data is flushed to the Clover database before running
* any query on it.
* <p/>
* <p>Note: The only use case where you would want to turn this off is if you're running your tests in a separate
* JVM. In that case the coverage data will be flushed by default upon the JVM shutdown and there would be no need
* to wait for the data to be flushed. As we can't control whether users want to fork their tests or not, we're
* offering this parameter to them.</p>
*
* @parameter expression="${maven.clover.waitForFlush}" default-value="true"
*/
private boolean waitForFlush;
/**
* Decide whether to generate an HTML report or not.
*
* @parameter default-value="true" expression="${maven.clover.generateHtml}"
*/
private boolean generateHtml;
/**
* Decide whether to generate a PDF report or not.
*
* @parameter default-value="false" expression="${maven.clover.generatePdf}"
*/
private boolean generatePdf;
/**
* Decide whether to generate a XML report or not.
*
* @parameter default-value="true" expression="${maven.clover.generateXml}"
*/
private boolean generateXml;
/**
* Decide whether to generate a JSON report or not.
*
* @parameter default-value="false" expression="${maven.clover.generateJson}"
*/
private boolean generateJson;
/**
* Decide whether to generate a Clover historical report or not.
*
* @parameter default-value="false" expression="${maven.clover.generateHistorical}"
*/
private boolean generateHistorical;
/**
* How to order coverage tables.
*
* @parameter default-value="PcCoveredAsc" expression="${maven.clover.orderBy}"
*/
private String orderBy;
/**
* Comma or space separated list of Clover somesrcexcluded (block, statement or method filers) to exclude when
* generating coverage reports.
*
* @parameter expression="${maven.clover.contextFilters}" default-value=""
*/
private String contextFilters;
/**
* Title of the report
*
* @parameter expression="${maven.clover.title}" default-value="${project.name} ${project.version}"
*/
private String title;
/**
* Title anchor of the report
*
* @parameter expression="${maven.clover.titleAnchor}" default-value="${project.url}"
*/
private String titleAnchor;
/**
* The charset to use in the html reports.
*
* @parameter expression="${maven.clover.charset}" default-value="UTF-8"
*/
private String charset;
/**
* <p>Note: This is passed by Maven and must not be configured by the user.</p>
*
* @component
*/
private Renderer siteRenderer;
/**
* The Maven project instance for the executing project.
* <p/>
* <p>Note: This is passed by Maven and must not be configured by the user.</p>
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* The projects in the reactor for aggregation report.
* <p/>
* <p>Note: This is passed by Maven and must not be configured by the user.</p>
*
* @parameter expression="${reactorProjects}"
* @readonly
*/
private List reactorProjects;
/**
* @parameter expression="${maven.clover.licenseLocation}"
* @see com.atlassian.maven.plugin.clover.internal.AbstractCloverMojo#licenseLocation
*/
private String licenseLocation;
/**
* @parameter expression="${maven.clover.license}"
* @see com.atlassian.maven.plugin.clover.internal.AbstractCloverMojo#license
*/
private String license;
/**
* Resource manager used to locate any Clover license file provided by the user.
*
* @component
*/
private ResourceManager resourceManager;
/**
* A span specifies the age of the coverage data that should be used when creating a report.
*
* @parameter expression="${maven.clover.span}"
*/
private String span = Interval.DEFAULT_SPAN.toString();
/**
* If set to true, a report will be generated even in the absence of coverage data.
*
* @parameter expression="${maven.clover.alwaysReport}" default-value="true"
*/
private boolean alwaysReport = true;
/**
* @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale)
*/
public void executeReport(Locale locale) throws MavenReportException {
if (!canGenerateReport()) {
getLog().info("No report being generated for this module.");
}
// only run the report once, on the very last project.
final MavenProject lastProject = (MavenProject) getReactorProjects().get(getReactorProjects().size() - 1);
final MavenProject thisProject = getProject();
if (isSingleCloverDatabase() && !thisProject.equals(lastProject)) {
getLog().info("Skipping report generation until the final project in the reactor.");
return;
}
// Register the Clover license
try {
AbstractCloverMojo.registerLicenseFile(this.project, this.resourceManager, this.licenseLocation, getLog(),
this.getClass().getClassLoader(), this.license);
}
catch (MojoExecutionException e) {
throw new MavenReportException("Failed to locate Clover license", e);
}
// Ensure the output directory exists
this.outputDirectory.mkdirs();
if (reportDescriptor == null) {
reportDescriptor = resolveCloverDescriptor();
} else if (!reportDescriptor.exists()){ // try finding this as a resource
try {
reportDescriptor = AbstractCloverMojo.getResourceAsFile(
project, resourceManager, reportDescriptor.getPath(), getLog(), this.getClass().getClassLoader());
} catch (MojoExecutionException e) {
throw new MavenReportException("Could not resolve report descriptor: " + reportDescriptor.getPath(), e);
}
}
getLog().info("Using Clover report descriptor: " + reportDescriptor.getAbsolutePath());
if(title != null && title.startsWith("Unnamed")) { // no project.name on the project
title = project.getArtifactId() + " " + project.getVersion();
}
File singleModuleCloverDatabase = new File(resolveCloverDatabase());
if (singleModuleCloverDatabase.exists()) {
createAllReportTypes(resolveCloverDatabase(), title);
}
File mergedCloverDatabase = new File(this.cloverMergeDatabase);
if (mergedCloverDatabase.exists()) {
createAllReportTypes(this.cloverMergeDatabase, title + " (Aggregated)");
}
}
/**
* Example of title prefixes: "Maven Clover", "Maven Aggregated Clover"
*/
private void createAllReportTypes(String database, String titlePrefix) throws MavenReportException {
final String outpath = outputDirectory.getAbsolutePath();
if (this.generateHtml) {
createReport(database, "html", titlePrefix, outpath, outpath, false);
}
if (this.generatePdf) {
createReport(database, "pdf", titlePrefix, outpath + "/clover.pdf", outpath + "/historical.pdf", true);
}
if (this.generateXml) {
createReport(database, "xml", titlePrefix, outpath + "/clover.xml", null, false);
}
if (this.generateJson) {
createReport(database, "json", titlePrefix, outpath, null, false);
}
}
/**
* Note: We use Clover's <code>clover-report</code> Ant task instead of the Clover CLI APIs because the CLI
* APIs are limited and do not support historical reports.
*/
private void createReport(String database, String format, String title, String output, String historyOut, boolean summary) {
final Project antProject = new Project();
antProject.init();
PropertyHelper propertyHelper = PropertyHelper.getPropertyHelper( antProject );
propertyHelper.setNext( new AntPropertyHelper( project, getLog() ) );
antProject.setUserProperty("ant.file", reportDescriptor.getAbsolutePath());
antProject.setCoreLoader(getClass().getClassLoader());
addMavenProperties(antProject);
antProject.setProperty("cloverdb", database);
antProject.setProperty("output", output);
antProject.setProperty("history", historyDir);
antProject.setProperty("title", title == null ? "" : title); // empty string will have it be ignore by clover
antProject.setProperty("titleAnchor", titleAnchor == null ? "" : titleAnchor);
final String projectDir = project.getBasedir().getPath();
antProject.setProperty("projectDir", projectDir);
antProject.setProperty("testPattern", "**/src/test/**");
antProject.setProperty("filter", contextFilters != null ? contextFilters : "");
antProject.setProperty("orderBy", orderBy);
antProject.setProperty("charset", charset);
antProject.setProperty("type", format);
antProject.setProperty("span", span);
antProject.setProperty("alwaysReport", "" + alwaysReport);
antProject.setProperty("summary", String.valueOf(summary));
if (historyOut != null) {
antProject.setProperty("historyout", historyOut);
}
AbstractCloverMojo.registerCloverAntTasks(antProject, getLog());
ProjectHelper.configureProject(antProject, reportDescriptor);
antProject.setBaseDir(project.getBasedir());
String target = isHistoricalDirectoryValid(output) && (historyOut != null) ? "historical" : "current";
antProject.executeTarget(target);
}
private void addMavenProperties(Project antProject) {
final Map properties = getProject().getProperties();
for (Iterator iterator = properties.entrySet().iterator(); iterator.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
getLog().debug("Setting Property: " + entry.getKey().toString() + " = " + entry.getValue().toString());
antProject.setProperty(entry.getKey().toString(), entry.getValue().toString());
}
}
private boolean isHistoricalDirectoryValid(String outFile) {
boolean isValid = false;
File dir = new File(this.historyDir);
if (dir.exists()) {
if (dir.listFiles().length > 0) {
isValid = true;
} else {
getLog().warn("No Clover historical data found in [" + this.historyDir + "], skipping Clover "
+ "historical report generation ([" + outFile + "])");
}
} else {
getLog().warn("Clover historical directory [" + this.historyDir + "] does not exist, skipping Clover "
+ "historical report generation ([" + outFile + "])");
}
return isValid;
}
/**
* @see org.apache.maven.reporting.MavenReport#getOutputName()
*/
public String getOutputName() {
return "clover/index";
}
/**
* @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale)
*/
public String getDescription(Locale locale) {
return getBundle(locale).getString("report.clover.description");
}
private static ResourceBundle getBundle(Locale locale) {
return ResourceBundle.getBundle("clover-report", locale, CloverReportMojo.class.getClassLoader());
}
/**
* @see org.apache.maven.reporting.AbstractMavenReport#getOutputDirectory()
*/
protected String getOutputDirectory() {
return this.outputDirectory.getAbsoluteFile().toString();
}
/**
* @see org.apache.maven.reporting.AbstractMavenReport#getSiteRenderer()
*/
protected Renderer getSiteRenderer() {
return this.siteRenderer;
}
/**
* @see org.apache.maven.reporting.AbstractMavenReport#getProject()
*/
public MavenProject getProject() {
return this.project;
}
/**
* @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale)
*/
public String getName(Locale locale) {
return getBundle(locale).getString("report.clover.name");
}
/**
* Always return true as we're using the report generated by Clover rather than creating our own report.
*
* @return true
*/
public boolean isExternalReport() {
return true;
}
/**
* Generate reports if a Clover module database or a Clover merged database exist.
*
* @return true if a project should be generated
* @see org.apache.maven.reporting.AbstractMavenReport#canGenerateReport()
*/
public boolean canGenerateReport() {
boolean canGenerate = false;
AbstractCloverMojo.waitForFlush(this.waitForFlush, this.flushInterval);
File singleModuleCloverDatabase = new File(resolveCloverDatabase());
File mergedCloverDatabase = new File(this.cloverMergeDatabase);
if (singleModuleCloverDatabase.exists() || mergedCloverDatabase.exists()) {
if (this.generateHtml || this.generatePdf || this.generateXml) {
canGenerate = true;
}
} else {
getLog().warn("No Clover database found, skipping report generation");
}
return canGenerate;
}
/**
* @see org.apache.maven.reporting.AbstractMavenReport#setReportOutputDirectory(java.io.File)
*/
public void setReportOutputDirectory(File reportOutputDirectory) {
if ((reportOutputDirectory != null) && (!reportOutputDirectory.getAbsolutePath().endsWith("clover"))) {
this.outputDirectory = new File(reportOutputDirectory, "clover");
} else {
this.outputDirectory = reportOutputDirectory;
}
}
/**
* The logic here is taken from AbstractSiteRenderingMojo#resolveSiteDescriptor in the maven-site-plugin.
* See also: http://docs.codehaus.org/display/MAVENUSER/Mojo+Developer+Cookbook
*
* @return the clover report configuration file to use
* @throws MavenReportException if at least the default file can't be resolved
*/
protected File resolveCloverDescriptor()
throws MavenReportException {
if (resolveReportDescriptor) {
getLog().info("Attempting to resolve the clover-report configuration as an xml artifact.");
Artifact artifact = artifactFactory.createArtifactWithClassifier(
project.getGroupId(),
project.getArtifactId(),
project.getVersion(),
"xml", "clover-report");
try {
artifactResolver.resolve(artifact, repositories, localRepository);
return artifact.getFile();
} catch (ArtifactResolutionException e) {
getLog().warn(e.getMessage(), e);
} catch (ArtifactNotFoundException e) {
getLog().warn(e.getMessage(), e);
}
}
try {
getLog().info("Using /default-clover-report descriptor.");
final File file = AbstractCloverMojo.getResourceAsFile(project,
resourceManager,
"/default-clover-report.xml",
getLog(),
this.getClass().getClassLoader());
file.deleteOnExit();
return file;
} catch (Exception e) {
throw new MavenReportException("Could not resolve default-clover-report.xml. " +
"Please try specifying this via the maven.clover.reportDescriptor property.", e);
}
}
public String getCloverDatabase()
{
return cloverDatabase;
}
public String resolveCloverDatabase()
{
return new ConfigUtil(this).resolveCloverDatabase();
}
public List getReactorProjects() {
return reactorProjects;
}
public boolean isSingleCloverDatabase() {
return this.singleCloverDatabase;
}
}