blob: 4cadbcd832853b5eb6206c78846e312097b296ba [file] [log] [blame]
package org.apache.maven.plugins.pmd;
/*
* 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.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import net.sourceforge.pmd.PMD;
import org.apache.maven.doxia.siterenderer.Renderer;
import org.apache.maven.model.ReportPlugin;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReport;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.PathTool;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.slf4j.bridge.SLF4JBridgeHandler;
/**
* Base class for the PMD reports.
*
* @author <a href="mailto:brett@apache.org">Brett Porter</a>
* @version $Id$
*/
public abstract class AbstractPmdReport
extends AbstractMavenReport
{
/**
* The output directory for the intermediate XML report.
*/
@Parameter( property = "project.build.directory", required = true )
protected File targetDirectory;
/**
* The output directory for the final HTML report. Note that this parameter is only evaluated if the goal is run
* directly from the command line or during the default 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( property = "project.reporting.outputDirectory", required = true )
protected File outputDirectory;
/**
* Site rendering component for generating the HTML report.
*/
@Component
private Renderer siteRenderer;
/**
* The project to analyse.
*/
@Parameter( defaultValue = "${project}", readonly = true, required = true )
protected MavenProject project;
/**
* Set the output format type, in addition to the HTML report. Must be one of: "none", "csv", "xml", "txt" or the
* full class name of the PMD renderer to use. See the net.sourceforge.pmd.renderers package javadoc for available
* renderers. XML is required if the pmd:check goal is being used.
*/
@Parameter( property = "format", defaultValue = "xml" )
protected String format = "xml";
/**
* Link the violation line numbers to the source xref. Links will be created automatically if the jxr plugin is
* being used.
*/
@Parameter( property = "linkXRef", defaultValue = "true" )
private boolean linkXRef;
/**
* Location of the Xrefs to link to.
*/
@Parameter( defaultValue = "${project.reporting.outputDirectory}/xref" )
private File xrefLocation;
/**
* Location of the Test Xrefs to link to.
*/
@Parameter( defaultValue = "${project.reporting.outputDirectory}/xref-test" )
private File xrefTestLocation;
/**
* A list of files to exclude from checking. Can contain Ant-style wildcards and double wildcards. Note that these
* exclusion patterns only operate on the path of a source file relative to its source root directory. In other
* words, files are excluded based on their package and/or class name. If you want to exclude entire source root
* directories, use the parameter <code>excludeRoots</code> instead.
*
* @since 2.2
*/
@Parameter
private List<String> excludes;
/**
* A list of files to include from checking. Can contain Ant-style wildcards and double wildcards. Defaults to
* **\/*.java.
*
* @since 2.2
*/
@Parameter
private List<String> includes;
/**
* Specifies the location of the source directories to be used for PMD.
* Defaults to <code>project.compileSourceRoots</code>.
* @since 3.7
*/
@Parameter( defaultValue = "${project.compileSourceRoots}" )
private List<String> compileSourceRoots;
/**
* The directories containing the test-sources to be used for PMD.
* Defaults to <code>project.testCompileSourceRoots</code>
* @since 3.7
*/
@Parameter( defaultValue = "${project.testCompileSourceRoots}" )
private List<String> testSourceRoots;
/**
* The project source directories that should be excluded.
*
* @since 2.2
*/
@Parameter
private File[] excludeRoots;
/**
* Run PMD on the tests.
*
* @since 2.2
*/
@Parameter( defaultValue = "false" )
protected boolean includeTests;
/**
* Whether to build an aggregated report at the root, or build individual reports.
*
* @since 2.2
*/
@Parameter( property = "aggregate", defaultValue = "false" )
protected boolean aggregate;
/**
* The file encoding to use when reading the Java sources.
*
* @since 2.3
*/
@Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
private String sourceEncoding;
/**
* The file encoding when writing non-HTML reports.
*
* @since 2.5
*/
@Parameter( property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}" )
private String outputEncoding;
/**
* The projects in the reactor for aggregation report.
*/
@Parameter( property = "reactorProjects", readonly = true )
protected List<MavenProject> reactorProjects;
/**
* Whether to include the xml files generated by PMD/CPD in the site.
*
* @since 3.0
*/
@Parameter( defaultValue = "false" )
protected boolean includeXmlInSite;
/**
* Skip the PMD/CPD report generation if there are no violations or duplications found. Defaults to
* <code>true</code>.
*
* @since 3.1
*/
@Parameter( defaultValue = "true" )
protected boolean skipEmptyReport;
/**
* File that lists classes and rules to be excluded from failures.
* For PMD, this is a properties file. For CPD, this
* is a text file that contains comma-separated lists of classes
* that are allowed to duplicate.
*
* @since 3.7
*/
@Parameter( property = "pmd.excludeFromFailureFile", defaultValue = "" )
protected String excludeFromFailureFile;
/**
* Redirect PMD log into maven log out.
* When enabled, the PMD log output is redirected to maven, so that
* it is visible in the console together with all the other log output.
* Also, if maven is started with the debug flag (<code>-X</code> or <code>--debug</code>),
* the PMD logger is also configured for debug.
*
* @since 3.9.0
*/
@Parameter( defaultValue = "true", property = "pmd.showPmdLog" )
protected boolean showPmdLog = true;
/**
* This holds a strong reference in case we configured the logger to
* redirect to slf4j. See {@link #showPmdLog}. Without a strong reference,
* the logger might be garbage collected and the redirect to slf4j is gone.
*/
private Logger julLogger;
/** The files that are being analyzed. */
protected Map<File, PmdFileInfo> filesToProcess;
/**
* {@inheritDoc}
*/
@Override
protected MavenProject getProject()
{
return project;
}
/**
* {@inheritDoc}
*/
@Override
protected Renderer getSiteRenderer()
{
return siteRenderer;
}
protected String constructXRefLocation( boolean test )
{
String location = null;
if ( linkXRef )
{
File xrefLoc = test ? xrefTestLocation : xrefLocation;
String relativePath =
PathTool.getRelativePath( outputDirectory.getAbsolutePath(), xrefLoc.getAbsolutePath() );
if ( StringUtils.isEmpty( relativePath ) )
{
relativePath = ".";
}
relativePath = relativePath + "/" + xrefLoc.getName();
if ( xrefLoc.exists() )
{
// XRef was already generated by manual execution of a lifecycle binding
location = relativePath;
}
else
{
// Not yet generated - check if the report is on its way
List<ReportPlugin> reportPlugins = project.getReportPlugins();
for ( ReportPlugin plugin : reportPlugins )
{
String artifactId = plugin.getArtifactId();
if ( "maven-jxr-plugin".equals( artifactId ) || "jxr-maven-plugin".equals( artifactId ) )
{
location = relativePath;
}
}
}
if ( location == null )
{
getLog().warn( "Unable to locate Source XRef to link to - DISABLED" );
}
}
return location;
}
/**
* Convenience method to get the list of files where the PMD tool will be executed
*
* @return a List of the files where the PMD tool will be executed
* @throws IOException If an I/O error occurs during construction of the
* canonical pathnames of the files
*/
protected Map<File, PmdFileInfo> getFilesToProcess()
throws IOException
{
if ( aggregate && !project.isExecutionRoot() )
{
return Collections.emptyMap();
}
if ( excludeRoots == null )
{
excludeRoots = new File[0];
}
Collection<File> excludeRootFiles = new HashSet<>( excludeRoots.length );
for ( File file : excludeRoots )
{
if ( file.isDirectory() )
{
excludeRootFiles.add( file );
}
}
List<PmdFileInfo> directories = new ArrayList<>();
if ( null == compileSourceRoots )
{
compileSourceRoots = project.getCompileSourceRoots();
}
if ( compileSourceRoots != null )
{
for ( String root : compileSourceRoots )
{
File sroot = new File( root );
if ( sroot.exists() )
{
String sourceXref = constructXRefLocation( false );
directories.add( new PmdFileInfo( project, sroot, sourceXref ) );
}
}
}
if ( null == testSourceRoots )
{
testSourceRoots = project.getTestCompileSourceRoots();
}
if ( includeTests && testSourceRoots != null )
{
for ( String root : testSourceRoots )
{
File sroot = new File( root );
if ( sroot.exists() )
{
String testXref = constructXRefLocation( true );
directories.add( new PmdFileInfo( project, sroot, testXref ) );
}
}
}
if ( aggregate )
{
for ( MavenProject localProject : reactorProjects )
{
List<String> localCompileSourceRoots = localProject.getCompileSourceRoots();
for ( String root : localCompileSourceRoots )
{
File sroot = new File( root );
if ( sroot.exists() )
{
String sourceXref = constructXRefLocation( false );
directories.add( new PmdFileInfo( localProject, sroot, sourceXref ) );
}
}
if ( includeTests )
{
List<String> localTestCompileSourceRoots = localProject.getTestCompileSourceRoots();
for ( String root : localTestCompileSourceRoots )
{
File sroot = new File( root );
if ( sroot.exists() )
{
String testXref = constructXRefLocation( true );
directories.add( new PmdFileInfo( localProject, sroot, testXref ) );
}
}
}
}
}
String excluding = getExcludes();
getLog().debug( "Exclusions: " + excluding );
String including = getIncludes();
getLog().debug( "Inclusions: " + including );
Map<File, PmdFileInfo> files = new TreeMap<>();
for ( PmdFileInfo finfo : directories )
{
getLog().debug( "Searching for files in directory " + finfo.getSourceDirectory().toString() );
File sourceDirectory = finfo.getSourceDirectory();
if ( sourceDirectory.isDirectory() && !isDirectoryExcluded( excludeRootFiles, sourceDirectory ) )
{
List<File> newfiles = FileUtils.getFiles( sourceDirectory, including, excluding );
for ( File newfile : newfiles )
{
files.put( newfile.getCanonicalFile(), finfo );
}
}
}
return files;
}
private boolean isDirectoryExcluded( Collection<File> excludeRootFiles, File sourceDirectoryToCheck )
{
boolean returnVal = false;
for ( File excludeDir : excludeRootFiles )
{
try
{
if ( sourceDirectoryToCheck.getCanonicalPath().startsWith( excludeDir.getCanonicalPath() ) )
{
getLog().debug( "Directory " + sourceDirectoryToCheck.getAbsolutePath()
+ " has been excluded as it matches excludeRoot "
+ excludeDir.getAbsolutePath() );
returnVal = true;
break;
}
}
catch ( IOException e )
{
getLog().warn( "Error while checking " + sourceDirectoryToCheck
+ " whether it should be excluded.", e );
}
}
return returnVal;
}
/**
* Gets the comma separated list of effective include patterns.
*
* @return The comma separated list of effective include patterns, never <code>null</code>.
*/
private String getIncludes()
{
Collection<String> patterns = new LinkedHashSet<>();
if ( includes != null )
{
patterns.addAll( includes );
}
if ( patterns.isEmpty() )
{
patterns.add( "**/*.java" );
}
return StringUtils.join( patterns.iterator(), "," );
}
/**
* Gets the comma separated list of effective exclude patterns.
*
* @return The comma separated list of effective exclude patterns, never <code>null</code>.
*/
private String getExcludes()
{
Collection<String> patterns = new LinkedHashSet<>( FileUtils.getDefaultExcludesAsList() );
if ( excludes != null )
{
patterns.addAll( excludes );
}
return StringUtils.join( patterns.iterator(), "," );
}
protected boolean isHtml()
{
return "html".equals( format );
}
protected boolean isXml()
{
return "xml".equals( format );
}
/**
* {@inheritDoc}
*/
@Override
public boolean canGenerateReport()
{
if ( aggregate && !project.isExecutionRoot() )
{
return false;
}
if ( "pom".equals( project.getPackaging() ) && !aggregate )
{
return false;
}
// if format is XML, we need to output it even if the file list is empty
// so the "check" goals can check for failures
if ( isXml() )
{
return true;
}
try
{
filesToProcess = getFilesToProcess();
if ( filesToProcess.isEmpty() )
{
return false;
}
}
catch ( IOException e )
{
getLog().error( e );
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
protected String getOutputDirectory()
{
return outputDirectory.getAbsolutePath();
}
protected String getSourceEncoding()
{
return sourceEncoding;
}
/**
* Gets the effective reporting output files encoding.
*
* @return The effective reporting output file encoding, never <code>null</code>.
* @since 2.5
*/
protected String getOutputEncoding()
{
return ( outputEncoding != null ) ? outputEncoding : ReaderFactory.UTF_8;
}
protected void setupPmdLogging()
{
if ( !showPmdLog )
{
return;
}
Logger logger = Logger.getLogger( "net.sourceforge.pmd" );
boolean slf4jBridgeAlreadyAdded = false;
for ( Handler handler : logger.getHandlers() )
{
if ( handler instanceof SLF4JBridgeHandler )
{
slf4jBridgeAlreadyAdded = true;
break;
}
}
if ( slf4jBridgeAlreadyAdded )
{
return;
}
SLF4JBridgeHandler handler = new SLF4JBridgeHandler();
SimpleFormatter formatter = new SimpleFormatter();
handler.setFormatter( formatter );
logger.setUseParentHandlers( false );
logger.addHandler( handler );
handler.setLevel( Level.ALL );
logger.setLevel( Level.ALL );
julLogger = logger;
julLogger.fine( "Configured jul-to-slf4j bridge for " + logger.getName() );
}
static String getPmdVersion()
{
try
{
return (String) PMD.class.getField( "VERSION" ).get( null );
}
catch ( IllegalAccessException e )
{
throw new RuntimeException( "PMD VERSION field not accessible", e );
}
catch ( NoSuchFieldException e )
{
throw new RuntimeException( "PMD VERSION field not found", e );
}
}
}