View Javadoc
1   package org.apache.maven.plugins.pmd;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.LinkedHashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.TreeMap;
32  import java.util.logging.Handler;
33  import java.util.logging.Level;
34  import java.util.logging.Logger;
35  import java.util.logging.SimpleFormatter;
36  
37  import net.sourceforge.pmd.PMD;
38  
39  import org.apache.maven.doxia.siterenderer.Renderer;
40  import org.apache.maven.model.ReportPlugin;
41  import org.apache.maven.plugins.annotations.Component;
42  import org.apache.maven.plugins.annotations.Parameter;
43  import org.apache.maven.project.MavenProject;
44  import org.apache.maven.reporting.AbstractMavenReport;
45  import org.codehaus.plexus.util.FileUtils;
46  import org.codehaus.plexus.util.PathTool;
47  import org.codehaus.plexus.util.ReaderFactory;
48  import org.codehaus.plexus.util.StringUtils;
49  import org.slf4j.bridge.SLF4JBridgeHandler;
50  
51  /**
52   * Base class for the PMD reports.
53   *
54   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
55   * @version $Id$
56   */
57  public abstract class AbstractPmdReport
58      extends AbstractMavenReport
59  {
60      /**
61       * The output directory for the intermediate XML report.
62       */
63      @Parameter( property = "project.build.directory", required = true )
64      protected File targetDirectory;
65  
66      /**
67       * The output directory for the final HTML report. Note that this parameter is only evaluated if the goal is run
68       * directly from the command line or during the default lifecycle. If the goal is run indirectly as part of a site
69       * generation, the output directory configured in the Maven Site Plugin is used instead.
70       */
71      @Parameter( property = "project.reporting.outputDirectory", required = true )
72      protected File outputDirectory;
73  
74      /**
75       * Site rendering component for generating the HTML report.
76       */
77      @Component
78      private Renderer siteRenderer;
79  
80      /**
81       * The project to analyse.
82       */
83      @Parameter( defaultValue = "${project}", readonly = true, required = true )
84      protected MavenProject project;
85  
86      /**
87       * Set the output format type, in addition to the HTML report. Must be one of: "none", "csv", "xml", "txt" or the
88       * full class name of the PMD renderer to use. See the net.sourceforge.pmd.renderers package javadoc for available
89       * renderers. XML is required if the pmd:check goal is being used.
90       */
91      @Parameter( property = "format", defaultValue = "xml" )
92      protected String format = "xml";
93  
94      /**
95       * Link the violation line numbers to the source xref. Links will be created automatically if the jxr plugin is
96       * being used.
97       */
98      @Parameter( property = "linkXRef", defaultValue = "true" )
99      private boolean linkXRef;
100 
101     /**
102      * Location of the Xrefs to link to.
103      */
104     @Parameter( defaultValue = "${project.reporting.outputDirectory}/xref" )
105     private File xrefLocation;
106 
107     /**
108      * Location of the Test Xrefs to link to.
109      */
110     @Parameter( defaultValue = "${project.reporting.outputDirectory}/xref-test" )
111     private File xrefTestLocation;
112 
113     /**
114      * A list of files to exclude from checking. Can contain Ant-style wildcards and double wildcards. Note that these
115      * exclusion patterns only operate on the path of a source file relative to its source root directory. In other
116      * words, files are excluded based on their package and/or class name. If you want to exclude entire source root
117      * directories, use the parameter <code>excludeRoots</code> instead.
118      *
119      * @since 2.2
120      */
121     @Parameter
122     private List<String> excludes;
123 
124     /**
125      * A list of files to include from checking. Can contain Ant-style wildcards and double wildcards. Defaults to
126      * **\/*.java.
127      *
128      * @since 2.2
129      */
130     @Parameter
131     private List<String> includes;
132 
133     /**
134      * Specifies the location of the source directories to be used for PMD.
135      * Defaults to <code>project.compileSourceRoots</code>.
136      * @since 3.7
137      */
138     @Parameter( defaultValue = "${project.compileSourceRoots}" )
139     private List<String> compileSourceRoots;
140 
141     /**
142      * The directories containing the test-sources to be used for PMD.
143      * Defaults to <code>project.testCompileSourceRoots</code>
144      * @since 3.7
145      */
146     @Parameter( defaultValue = "${project.testCompileSourceRoots}" )
147     private List<String> testSourceRoots;
148 
149     /**
150      * The project source directories that should be excluded.
151      *
152      * @since 2.2
153      */
154     @Parameter
155     private File[] excludeRoots;
156 
157     /**
158      * Run PMD on the tests.
159      *
160      * @since 2.2
161      */
162     @Parameter( defaultValue = "false" )
163     protected boolean includeTests;
164 
165     /**
166      * Whether to build an aggregated report at the root, or build individual reports.
167      *
168      * @since 2.2
169      */
170     @Parameter( property = "aggregate", defaultValue = "false" )
171     protected boolean aggregate;
172 
173     /**
174      * The file encoding to use when reading the Java sources.
175      *
176      * @since 2.3
177      */
178     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
179     private String sourceEncoding;
180 
181     /**
182      * The file encoding when writing non-HTML reports.
183      *
184      * @since 2.5
185      */
186     @Parameter( property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}" )
187     private String outputEncoding;
188 
189     /**
190      * The projects in the reactor for aggregation report.
191      */
192     @Parameter( property = "reactorProjects", readonly = true )
193     protected List<MavenProject> reactorProjects;
194 
195     /**
196      * Whether to include the xml files generated by PMD/CPD in the site.
197      *
198      * @since 3.0
199      */
200     @Parameter( defaultValue = "false" )
201     protected boolean includeXmlInSite;
202 
203     /**
204      * Skip the PMD/CPD report generation if there are no violations or duplications found. Defaults to
205      * <code>false</code>.
206      *
207      * <p>Note: the default value was changed from <code>true</code> to <code>false</code> with version 3.13.0.
208      *
209      * @since 3.1
210      */
211     @Parameter( defaultValue = "false" )
212     protected boolean skipEmptyReport;
213 
214     /**
215      * File that lists classes and rules to be excluded from failures.
216      * For PMD, this is a properties file. For CPD, this
217      * is a text file that contains comma-separated lists of classes
218      * that are allowed to duplicate.
219      *
220      * @since 3.7
221      */
222     @Parameter( property = "pmd.excludeFromFailureFile", defaultValue = "" )
223     protected String excludeFromFailureFile;
224 
225     /**
226      * Redirect PMD log into maven log out.
227      * When enabled, the PMD log output is redirected to maven, so that
228      * it is visible in the console together with all the other log output.
229      * Also, if maven is started with the debug flag (<code>-X</code> or <code>--debug</code>),
230      * the PMD logger is also configured for debug.
231      *
232      * @since 3.9.0
233      */
234     @Parameter( defaultValue = "true", property = "pmd.showPmdLog" )
235     protected boolean showPmdLog = true;
236 
237     /**
238      * This holds a strong reference in case we configured the logger to
239      * redirect to slf4j. See {@link #showPmdLog}. Without a strong reference,
240      * the logger might be garbage collected and the redirect to slf4j is gone.
241      */
242     private Logger julLogger;
243 
244     /** The files that are being analyzed. */
245     protected Map<File, PmdFileInfo> filesToProcess;
246 
247     /**
248      * {@inheritDoc}
249      */
250     @Override
251     protected MavenProject getProject()
252     {
253         return project;
254     }
255 
256     /**
257      * {@inheritDoc}
258      */
259     @Override
260     protected Renderer getSiteRenderer()
261     {
262         return siteRenderer;
263     }
264 
265     protected String constructXRefLocation( boolean test )
266     {
267         String location = null;
268         if ( linkXRef )
269         {
270             File xrefLoc = test ? xrefTestLocation : xrefLocation;
271 
272             String relativePath =
273                 PathTool.getRelativePath( outputDirectory.getAbsolutePath(), xrefLoc.getAbsolutePath() );
274             if ( StringUtils.isEmpty( relativePath ) )
275             {
276                 relativePath = ".";
277             }
278             relativePath = relativePath + "/" + xrefLoc.getName();
279             if ( xrefLoc.exists() )
280             {
281                 // XRef was already generated by manual execution of a lifecycle binding
282                 location = relativePath;
283             }
284             else
285             {
286                 // Not yet generated - check if the report is on its way
287                 List<ReportPlugin> reportPlugins = project.getReportPlugins();
288                 for ( ReportPlugin plugin : reportPlugins )
289                 {
290                     String artifactId = plugin.getArtifactId();
291                     if ( "maven-jxr-plugin".equals( artifactId ) || "jxr-maven-plugin".equals( artifactId ) )
292                     {
293                         location = relativePath;
294                     }
295                 }
296             }
297 
298             if ( location == null )
299             {
300                 getLog().warn( "Unable to locate Source XRef to link to - DISABLED" );
301             }
302         }
303         return location;
304     }
305 
306     /**
307      * Convenience method to get the list of files where the PMD tool will be executed
308      *
309      * @return a List of the files where the PMD tool will be executed
310      * @throws IOException If an I/O error occurs during construction of the
311      *                     canonical pathnames of the files
312      */
313     protected Map<File, PmdFileInfo> getFilesToProcess()
314         throws IOException
315     {
316         if ( aggregate && !project.isExecutionRoot() )
317         {
318             return Collections.emptyMap();
319         }
320 
321         if ( excludeRoots == null )
322         {
323             excludeRoots = new File[0];
324         }
325 
326         Collection<File> excludeRootFiles = new HashSet<>( excludeRoots.length );
327 
328         for ( File file : excludeRoots )
329         {
330             if ( file.isDirectory() )
331             {
332                 excludeRootFiles.add( file );
333             }
334         }
335 
336         List<PmdFileInfo> directories = new ArrayList<>();
337 
338         if ( null == compileSourceRoots )
339         {
340             compileSourceRoots = project.getCompileSourceRoots();
341         }
342         if ( compileSourceRoots != null )
343         {
344             for ( String root : compileSourceRoots )
345             {
346                 File sroot = new File( root );
347                 if ( sroot.exists() )
348                 {
349                     String sourceXref = constructXRefLocation( false );
350                     directories.add( new PmdFileInfo( project, sroot, sourceXref ) );
351                 }
352             }
353         }
354 
355         if ( null == testSourceRoots )
356         {
357             testSourceRoots = project.getTestCompileSourceRoots();
358         }
359         if ( includeTests && testSourceRoots != null )
360         {
361             for ( String root : testSourceRoots )
362             {
363                 File sroot = new File( root );
364                 if ( sroot.exists() )
365                 {
366                     String testXref = constructXRefLocation( true );
367                     directories.add( new PmdFileInfo( project, sroot, testXref ) );
368                 }
369             }
370         }
371         if ( aggregate )
372         {
373             for ( MavenProject localProject : reactorProjects )
374             {
375                 List<String> localCompileSourceRoots = localProject.getCompileSourceRoots();
376                 for ( String root : localCompileSourceRoots )
377                 {
378                     File sroot = new File( root );
379                     if ( sroot.exists() )
380                     {
381                         String sourceXref = constructXRefLocation( false );
382                         directories.add( new PmdFileInfo( localProject, sroot, sourceXref ) );
383                     }
384                 }
385                 if ( includeTests )
386                 {
387                     List<String> localTestCompileSourceRoots = localProject.getTestCompileSourceRoots();
388                     for ( String root : localTestCompileSourceRoots )
389                     {
390                         File sroot = new File( root );
391                         if ( sroot.exists() )
392                         {
393                             String testXref = constructXRefLocation( true );
394                             directories.add( new PmdFileInfo( localProject, sroot, testXref ) );
395                         }
396                     }
397                 }
398             }
399 
400         }
401 
402         String excluding = getExcludes();
403         getLog().debug( "Exclusions: " + excluding );
404         String including = getIncludes();
405         getLog().debug( "Inclusions: " + including );
406 
407         Map<File, PmdFileInfo> files = new TreeMap<>();
408 
409         for ( PmdFileInfo finfo : directories )
410         {
411             getLog().debug( "Searching for files in directory " + finfo.getSourceDirectory().toString() );
412             File sourceDirectory = finfo.getSourceDirectory();
413             if ( sourceDirectory.isDirectory() && !isDirectoryExcluded( excludeRootFiles, sourceDirectory ) )
414             {
415                 List<File> newfiles = FileUtils.getFiles( sourceDirectory, including, excluding );
416                 for ( File newfile : newfiles )
417                 {
418                     files.put( newfile.getCanonicalFile(), finfo );
419                 }
420             }
421         }
422 
423         return files;
424     }
425 
426     private boolean isDirectoryExcluded( Collection<File> excludeRootFiles, File sourceDirectoryToCheck )
427     {
428         boolean returnVal = false;
429         for ( File excludeDir : excludeRootFiles )
430         {
431             try
432             {
433                 if ( sourceDirectoryToCheck.getCanonicalPath().startsWith( excludeDir.getCanonicalPath() ) )
434                 {
435                     getLog().debug( "Directory " + sourceDirectoryToCheck.getAbsolutePath()
436                                         + " has been excluded as it matches excludeRoot "
437                                         + excludeDir.getAbsolutePath() );
438                     returnVal = true;
439                     break;
440                 }
441             }
442             catch ( IOException e )
443             {
444                 getLog().warn( "Error while checking " + sourceDirectoryToCheck
445                                + " whether it should be excluded.", e );
446             }
447         }
448         return returnVal;
449     }
450 
451     /**
452      * Gets the comma separated list of effective include patterns.
453      *
454      * @return The comma separated list of effective include patterns, never <code>null</code>.
455      */
456     private String getIncludes()
457     {
458         Collection<String> patterns = new LinkedHashSet<>();
459         if ( includes != null )
460         {
461             patterns.addAll( includes );
462         }
463         if ( patterns.isEmpty() )
464         {
465             patterns.add( "**/*.java" );
466         }
467         return StringUtils.join( patterns.iterator(), "," );
468     }
469 
470     /**
471      * Gets the comma separated list of effective exclude patterns.
472      *
473      * @return The comma separated list of effective exclude patterns, never <code>null</code>.
474      */
475     private String getExcludes()
476     {
477         Collection<String> patterns = new LinkedHashSet<>( FileUtils.getDefaultExcludesAsList() );
478         if ( excludes != null )
479         {
480             patterns.addAll( excludes );
481         }
482         return StringUtils.join( patterns.iterator(), "," );
483     }
484 
485     protected boolean isHtml()
486     {
487         return "html".equals( format );
488     }
489 
490     protected boolean isXml()
491     {
492         return "xml".equals( format );
493     }
494 
495     /**
496      * {@inheritDoc}
497      */
498     @Override
499     public boolean canGenerateReport()
500     {
501         if ( aggregate && !project.isExecutionRoot() )
502         {
503             return false;
504         }
505 
506         if ( "pom".equals( project.getPackaging() ) && !aggregate )
507         {
508             return false;
509         }
510 
511         // if format is XML, we need to output it even if the file list is empty
512         // so the "check" goals can check for failures
513         if ( isXml() )
514         {
515             return true;
516         }
517         try
518         {
519             filesToProcess = getFilesToProcess();
520             if ( filesToProcess.isEmpty() )
521             {
522                 return false;
523             }
524         }
525         catch ( IOException e )
526         {
527             getLog().error( e );
528         }
529         return true;
530     }
531 
532     /**
533      * {@inheritDoc}
534      */
535     @Override
536     protected String getOutputDirectory()
537     {
538         return outputDirectory.getAbsolutePath();
539     }
540 
541     protected String getSourceEncoding()
542     {
543         return sourceEncoding;
544     }
545 
546     /**
547      * Gets the effective reporting output files encoding.
548      *
549      * @return The effective reporting output file encoding, never <code>null</code>.
550      * @since 2.5
551      */
552     protected String getOutputEncoding()
553     {
554         return ( outputEncoding != null ) ? outputEncoding : ReaderFactory.UTF_8;
555     }
556 
557     protected void setupPmdLogging()
558     {
559         if ( !showPmdLog )
560         {
561             return;
562         }
563 
564         Logger logger = Logger.getLogger( "net.sourceforge.pmd" );
565 
566         boolean slf4jBridgeAlreadyAdded = false;
567         for ( Handler handler : logger.getHandlers() )
568         {
569             if ( handler instanceof SLF4JBridgeHandler )
570             {
571                 slf4jBridgeAlreadyAdded = true;
572                 break;
573             }
574         }
575 
576         if ( slf4jBridgeAlreadyAdded )
577         {
578             return;
579         }
580 
581         SLF4JBridgeHandler handler = new SLF4JBridgeHandler();
582         SimpleFormatter formatter = new SimpleFormatter();
583         handler.setFormatter( formatter );
584         logger.setUseParentHandlers( false );
585         logger.addHandler( handler );
586         handler.setLevel( Level.ALL );
587         logger.setLevel( Level.ALL );
588         julLogger = logger;
589         julLogger.fine(  "Configured jul-to-slf4j bridge for " + logger.getName() );
590     }
591 
592     static String getPmdVersion()
593     {
594         try
595         {
596             return (String) PMD.class.getField( "VERSION" ).get( null );
597         }
598         catch ( IllegalAccessException e )
599         {
600             throw new RuntimeException( "PMD VERSION field not accessible", e );
601         }
602         catch ( NoSuchFieldException e )
603         {
604             throw new RuntimeException( "PMD VERSION field not found", e );
605         }
606     }
607 }