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