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>true</code>.
206      *
207      * @since 3.1
208      */
209     @Parameter( defaultValue = "true" )
210     protected boolean skipEmptyReport;
211 
212     /**
213      * File that lists classes and rules to be excluded from failures.
214      * For PMD, this is a properties file. For CPD, this
215      * is a text file that contains comma-separated lists of classes
216      * that are allowed to duplicate.
217      *
218      * @since 3.7
219      */
220     @Parameter( property = "pmd.excludeFromFailureFile", defaultValue = "" )
221     protected String excludeFromFailureFile;
222 
223     /**
224      * Redirect PMD log into maven log out.
225      * When enabled, the PMD log output is redirected to maven, so that
226      * it is visible in the console together with all the other log output.
227      * Also, if maven is started with the debug flag (<code>-X</code> or <code>--debug</code>),
228      * the PMD logger is also configured for debug.
229      *
230      * @since 3.9.0
231      */
232     @Parameter( defaultValue = "true", property = "pmd.showPmdLog" )
233     protected boolean showPmdLog = true;
234 
235     /** The files that are being analyzed. */
236     protected Map<File, PmdFileInfo> filesToProcess;
237 
238     /**
239      * {@inheritDoc}
240      */
241     @Override
242     protected MavenProject getProject()
243     {
244         return project;
245     }
246 
247     /**
248      * {@inheritDoc}
249      */
250     @Override
251     protected Renderer getSiteRenderer()
252     {
253         return siteRenderer;
254     }
255 
256     protected String constructXRefLocation( boolean test )
257     {
258         String location = null;
259         if ( linkXRef )
260         {
261             File xrefLoc = test ? xrefTestLocation : xrefLocation;
262 
263             String relativePath =
264                 PathTool.getRelativePath( outputDirectory.getAbsolutePath(), xrefLoc.getAbsolutePath() );
265             if ( StringUtils.isEmpty( relativePath ) )
266             {
267                 relativePath = ".";
268             }
269             relativePath = relativePath + "/" + xrefLoc.getName();
270             if ( xrefLoc.exists() )
271             {
272                 // XRef was already generated by manual execution of a lifecycle binding
273                 location = relativePath;
274             }
275             else
276             {
277                 // Not yet generated - check if the report is on its way
278                 List<ReportPlugin> reportPlugins = project.getReportPlugins();
279                 for ( ReportPlugin plugin : reportPlugins )
280                 {
281                     String artifactId = plugin.getArtifactId();
282                     if ( "maven-jxr-plugin".equals( artifactId ) || "jxr-maven-plugin".equals( artifactId ) )
283                     {
284                         location = relativePath;
285                     }
286                 }
287             }
288 
289             if ( location == null )
290             {
291                 getLog().warn( "Unable to locate Source XRef to link to - DISABLED" );
292             }
293         }
294         return location;
295     }
296 
297     /**
298      * Convenience method to get the list of files where the PMD tool will be executed
299      *
300      * @return a List of the files where the PMD tool will be executed
301      * @throws IOException If an I/O error occurs during construction of the
302      *                     canonical pathnames of the files
303      */
304     protected Map<File, PmdFileInfo> getFilesToProcess()
305         throws IOException
306     {
307         if ( aggregate && !project.isExecutionRoot() )
308         {
309             return Collections.emptyMap();
310         }
311 
312         if ( excludeRoots == null )
313         {
314             excludeRoots = new File[0];
315         }
316 
317         Collection<File> excludeRootFiles = new HashSet<>( excludeRoots.length );
318 
319         for ( File file : excludeRoots )
320         {
321             if ( file.isDirectory() )
322             {
323                 excludeRootFiles.add( file );
324             }
325         }
326 
327         List<PmdFileInfo> directories = new ArrayList<>();
328 
329         if ( null == compileSourceRoots )
330         {
331             compileSourceRoots = project.getCompileSourceRoots();
332         }
333         if ( compileSourceRoots != null )
334         {
335             for ( String root : compileSourceRoots )
336             {
337                 File sroot = new File( root );
338                 if ( sroot.exists() )
339                 {
340                     String sourceXref = constructXRefLocation( false );
341                     directories.add( new PmdFileInfo( project, sroot, sourceXref ) );
342                 }
343             }
344         }
345 
346         if ( null == testSourceRoots )
347         {
348             testSourceRoots = project.getTestCompileSourceRoots();
349         }
350         if ( includeTests && testSourceRoots != null )
351         {
352             for ( String root : testSourceRoots )
353             {
354                 File sroot = new File( root );
355                 if ( sroot.exists() )
356                 {
357                     String testXref = constructXRefLocation( true );
358                     directories.add( new PmdFileInfo( project, sroot, testXref ) );
359                 }
360             }
361         }
362         if ( aggregate )
363         {
364             for ( MavenProject localProject : reactorProjects )
365             {
366                 List<String> localCompileSourceRoots = localProject.getCompileSourceRoots();
367                 for ( String root : localCompileSourceRoots )
368                 {
369                     File sroot = new File( root );
370                     if ( sroot.exists() )
371                     {
372                         String sourceXref = constructXRefLocation( false );
373                         directories.add( new PmdFileInfo( localProject, sroot, sourceXref ) );
374                     }
375                 }
376                 if ( includeTests )
377                 {
378                     List<String> localTestCompileSourceRoots = localProject.getTestCompileSourceRoots();
379                     for ( String root : localTestCompileSourceRoots )
380                     {
381                         File sroot = new File( root );
382                         if ( sroot.exists() )
383                         {
384                             String testXref = constructXRefLocation( true );
385                             directories.add( new PmdFileInfo( localProject, sroot, testXref ) );
386                         }
387                     }
388                 }
389             }
390 
391         }
392 
393         String excluding = getExcludes();
394         getLog().debug( "Exclusions: " + excluding );
395         String including = getIncludes();
396         getLog().debug( "Inclusions: " + including );
397 
398         Map<File, PmdFileInfo> files = new TreeMap<>();
399 
400         for ( PmdFileInfo finfo : directories )
401         {
402             getLog().debug( "Searching for files in directory " + finfo.getSourceDirectory().toString() );
403             File sourceDirectory = finfo.getSourceDirectory();
404             if ( sourceDirectory.isDirectory() && !isDirectoryExcluded( excludeRootFiles, sourceDirectory ) )
405             {
406                 List<File> newfiles = FileUtils.getFiles( sourceDirectory, including, excluding );
407                 for ( File newfile : newfiles )
408                 {
409                     files.put( newfile.getCanonicalFile(), finfo );
410                 }
411             }
412         }
413 
414         return files;
415     }
416 
417     private boolean isDirectoryExcluded( Collection<File> excludeRootFiles, File sourceDirectoryToCheck )
418     {
419         boolean returnVal = false;
420         for ( File excludeDir : excludeRootFiles )
421         {
422             try
423             {
424                 if ( sourceDirectoryToCheck.getCanonicalPath().startsWith( excludeDir.getCanonicalPath() ) )
425                 {
426                     getLog().debug( "Directory " + sourceDirectoryToCheck.getAbsolutePath()
427                                         + " has been excluded as it matches excludeRoot "
428                                         + excludeDir.getAbsolutePath() );
429                     returnVal = true;
430                     break;
431                 }
432             }
433             catch ( IOException e )
434             {
435                 getLog().warn( "Error while checking " + sourceDirectoryToCheck
436                                + " whether it should be excluded.", e );
437             }
438         }
439         return returnVal;
440     }
441 
442     /**
443      * Gets the comma separated list of effective include patterns.
444      *
445      * @return The comma separated list of effective include patterns, never <code>null</code>.
446      */
447     private String getIncludes()
448     {
449         Collection<String> patterns = new LinkedHashSet<>();
450         if ( includes != null )
451         {
452             patterns.addAll( includes );
453         }
454         if ( patterns.isEmpty() )
455         {
456             patterns.add( "**/*.java" );
457         }
458         return StringUtils.join( patterns.iterator(), "," );
459     }
460 
461     /**
462      * Gets the comma separated list of effective exclude patterns.
463      *
464      * @return The comma separated list of effective exclude patterns, never <code>null</code>.
465      */
466     private String getExcludes()
467     {
468         Collection<String> patterns = new LinkedHashSet<>( FileUtils.getDefaultExcludesAsList() );
469         if ( excludes != null )
470         {
471             patterns.addAll( excludes );
472         }
473         return StringUtils.join( patterns.iterator(), "," );
474     }
475 
476     protected boolean isHtml()
477     {
478         return "html".equals( format );
479     }
480 
481     protected boolean isXml()
482     {
483         return "xml".equals( format );
484     }
485 
486     /**
487      * {@inheritDoc}
488      */
489     @Override
490     public boolean canGenerateReport()
491     {
492         if ( aggregate && !project.isExecutionRoot() )
493         {
494             return false;
495         }
496 
497         if ( "pom".equals( project.getPackaging() ) && !aggregate )
498         {
499             return false;
500         }
501 
502         // if format is XML, we need to output it even if the file list is empty
503         // so the "check" goals can check for failures
504         if ( isXml() )
505         {
506             return true;
507         }
508         try
509         {
510             filesToProcess = getFilesToProcess();
511             if ( filesToProcess.isEmpty() )
512             {
513                 return false;
514             }
515         }
516         catch ( IOException e )
517         {
518             getLog().error( e );
519         }
520         return true;
521     }
522 
523     /**
524      * {@inheritDoc}
525      */
526     @Override
527     protected String getOutputDirectory()
528     {
529         return outputDirectory.getAbsolutePath();
530     }
531 
532     protected String getSourceEncoding()
533     {
534         return sourceEncoding;
535     }
536 
537     /**
538      * Gets the effective reporting output files encoding.
539      *
540      * @return The effective reporting output file encoding, never <code>null</code>.
541      * @since 2.5
542      */
543     protected String getOutputEncoding()
544     {
545         return ( outputEncoding != null ) ? outputEncoding : ReaderFactory.UTF_8;
546     }
547 
548     protected void setupPmdLogging()
549     {
550         if ( !showPmdLog )
551         {
552             return;
553         }
554 
555         Logger logger = Logger.getLogger( "net.sourceforge.pmd" );
556 
557         boolean slf4jBridgeAlreadyAdded = false;
558         for ( Handler handler : logger.getHandlers() )
559         {
560             if ( handler instanceof SLF4JBridgeHandler )
561             {
562                 slf4jBridgeAlreadyAdded = true;
563                 break;
564             }
565         }
566 
567         if ( slf4jBridgeAlreadyAdded )
568         {
569             return;
570         }
571 
572         SLF4JBridgeHandler handler = new SLF4JBridgeHandler();
573         SimpleFormatter formatter = new SimpleFormatter();
574         handler.setFormatter( formatter );
575         logger.setUseParentHandlers( false );
576         logger.addHandler( handler );
577         handler.setLevel( Level.ALL );
578         logger.setLevel( Level.ALL );
579         getLog().debug( "Configured jul-to-slf4j bridge for " + logger.getName() );
580     }
581 
582     static String getPmdVersion()
583     {
584         try
585         {
586             return (String) PMD.class.getField( "VERSION" ).get( null );
587         }
588         catch ( IllegalAccessException e )
589         {
590             throw new RuntimeException( "PMD VERSION field not accessible", e );
591         }
592         catch ( NoSuchFieldException e )
593         {
594             throw new RuntimeException( "PMD VERSION field not found", e );
595         }
596     }
597 }