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