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.lang.reflect.InvocationTargetException;
25  import java.lang.reflect.Method;
26  import java.nio.file.Path;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.LinkedHashSet;
33  import java.util.LinkedList;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Set;
37  import java.util.TreeMap;
38  
39  import org.apache.maven.doxia.siterenderer.Renderer;
40  import org.apache.maven.execution.MavenSession;
41  import org.apache.maven.model.ReportPlugin;
42  import org.apache.maven.model.Reporting;
43  import org.apache.maven.plugins.annotations.Component;
44  import org.apache.maven.plugins.annotations.Parameter;
45  import org.apache.maven.project.MavenProject;
46  import org.apache.maven.reporting.AbstractMavenReport;
47  import org.apache.maven.reporting.MavenReportException;
48  import org.apache.maven.toolchain.Toolchain;
49  import org.apache.maven.toolchain.ToolchainManager;
50  import org.codehaus.plexus.util.FileUtils;
51  import org.codehaus.plexus.util.PathTool;
52  import org.codehaus.plexus.util.ReaderFactory;
53  import org.codehaus.plexus.util.StringUtils;
54  
55  import net.sourceforge.pmd.PMDVersion;
56  
57  /**
58   * Base class for the PMD reports.
59   *
60   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
61   * @version $Id$
62   */
63  public abstract class AbstractPmdReport
64      extends AbstractMavenReport
65  {
66      // ----------------------------------------------------------------------
67      // Configurables
68      // ----------------------------------------------------------------------
69  
70      /**
71       * The output directory for the intermediate XML report.
72       */
73      @Parameter( property = "project.build.directory", required = true )
74      protected File targetDirectory;
75  
76      /**
77       * The output directory for the final HTML report. Note that this parameter is only evaluated if the goal is run
78       * directly from the command line or during the default lifecycle. If the goal is run indirectly as part of a site
79       * generation, the output directory configured in the Maven Site Plugin is used instead.
80       */
81      @Parameter( property = "project.reporting.outputDirectory", required = true )
82      protected File outputDirectory;
83  
84      /**
85       * Set the output format type, in addition to the HTML report. Must be one of: "none", "csv", "xml", "txt" or the
86       * full class name of the PMD renderer to use. See the net.sourceforge.pmd.renderers package javadoc for available
87       * renderers. XML is produced in any case, since this format is needed
88       * for the check goals (pmd:check, pmd:aggregator-check, pmd:cpd-check, pmd:aggregator-cpd-check).
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      * @deprecated since 3.15.0 Use the goals <code>pmd:aggregate-pmd</code> and <code>pmd:aggregate-cpd</code>
169      * instead.
170      */
171     @Parameter( property = "aggregate", defaultValue = "false" )
172     @Deprecated
173     protected boolean aggregate;
174 
175     /**
176      * The file encoding to use when reading the Java sources.
177      *
178      * @since 2.3
179      */
180     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
181     private String sourceEncoding;
182 
183     /**
184      * The file encoding when writing non-HTML reports.
185      *
186      * @since 2.5
187      */
188     @Parameter( property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}" )
189     private String outputEncoding;
190 
191     /**
192      * Whether to include the xml files generated by PMD/CPD in the site.
193      *
194      * @since 3.0
195      */
196     @Parameter( defaultValue = "false" )
197     protected boolean includeXmlInSite;
198 
199     /**
200      * Skip the PMD/CPD report generation if there are no violations or duplications found. Defaults to
201      * <code>false</code>.
202      *
203      * <p>Note: the default value was changed from <code>true</code> to <code>false</code> with version 3.13.0.
204      *
205      * @since 3.1
206      */
207     @Parameter( defaultValue = "false" )
208     protected boolean skipEmptyReport;
209 
210     /**
211      * File that lists classes and rules to be excluded from failures.
212      * For PMD, this is a properties file. For CPD, this
213      * is a text file that contains comma-separated lists of classes
214      * that are allowed to duplicate.
215      *
216      * @since 3.7
217      */
218     @Parameter( property = "pmd.excludeFromFailureFile", defaultValue = "" )
219     protected String excludeFromFailureFile;
220 
221     /**
222      * Redirect PMD log into maven log out.
223      * When enabled, the PMD log output is redirected to maven, so that
224      * it is visible in the console together with all the other log output.
225      * Also, if maven is started with the debug flag (<code>-X</code> or <code>--debug</code>),
226      * the PMD logger is also configured for debug.
227      *
228      * @since 3.9.0
229      */
230     @Parameter( defaultValue = "true", property = "pmd.showPmdLog" )
231     protected boolean showPmdLog = true;
232 
233     /**
234      * <p>
235      * Allow for configuration of the jvm used to run PMD via maven toolchains.
236      * This permits a configuration where the project is built with one jvm and PMD is executed with another.
237      * This overrules the toolchain selected by the maven-toolchain-plugin.
238      * </p>
239      *
240      * <p>Examples:</p>
241      * (see <a href="https://maven.apache.org/guides/mini/guide-using-toolchains.html">
242      *     Guide to Toolchains</a> for more info)
243      *
244      * <pre>
245      * {@code
246      *    <configuration>
247      *        ...
248      *        <jdkToolchain>
249      *            <version>1.11</version>
250      *        </jdkToolchain>
251      *    </configuration>
252      *
253      *    <configuration>
254      *        ...
255      *        <jdkToolchain>
256      *            <version>1.8</version>
257      *            <vendor>zulu</vendor>
258      *        </jdkToolchain>
259      *    </configuration>
260      *    }
261      * </pre>
262      *
263      * <strong>note:</strong> requires at least Maven 3.3.1
264      *
265      * @since 3.14.0
266      */
267     @Parameter
268     private Map<String, String> jdkToolchain;
269 
270     // ----------------------------------------------------------------------
271     // Read-only parameters
272     // ----------------------------------------------------------------------
273 
274     /**
275      * The project to analyse.
276      */
277     @Parameter( defaultValue = "${project}", readonly = true, required = true )
278     protected MavenProject project;
279 
280     /**
281      * The projects in the reactor for aggregation report.
282      */
283     @Parameter( property = "reactorProjects", readonly = true )
284     protected List<MavenProject> reactorProjects;
285 
286     /**
287      * The current build session instance. This is used for
288      * toolchain manager API calls and for dependency resolver API calls.
289      */
290     @Parameter( defaultValue = "${session}", required = true, readonly = true )
291     protected MavenSession session;
292 
293     /**
294      * Site rendering component for generating the HTML report.
295      */
296     @Component
297     private Renderer siteRenderer;
298 
299     @Component
300     private ToolchainManager toolchainManager;
301 
302     /** The files that are being analyzed. */
303     protected Map<File, PmdFileInfo> filesToProcess;
304 
305     /**
306      * {@inheritDoc}
307      */
308     @Override
309     protected MavenProject getProject()
310     {
311         return project;
312     }
313 
314     /**
315      * {@inheritDoc}
316      */
317     @Override
318     protected Renderer getSiteRenderer()
319     {
320         return siteRenderer;
321     }
322 
323     protected String constructXRefLocation( boolean test )
324     {
325         String location = null;
326         if ( linkXRef )
327         {
328             File xrefLoc = test ? xrefTestLocation : xrefLocation;
329 
330             String relativePath =
331                 PathTool.getRelativePath( outputDirectory.getAbsolutePath(), xrefLoc.getAbsolutePath() );
332             if ( StringUtils.isEmpty( relativePath ) )
333             {
334                 relativePath = ".";
335             }
336             relativePath = relativePath + "/" + xrefLoc.getName();
337             if ( xrefLoc.exists() )
338             {
339                 // XRef was already generated by manual execution of a lifecycle binding
340                 location = relativePath;
341             }
342             else
343             {
344                 // Not yet generated - check if the report is on its way
345                 Reporting reporting = project.getModel().getReporting();
346                 List<ReportPlugin> reportPlugins = reporting != null
347                         ? reporting.getPlugins()
348                         : Collections.<ReportPlugin>emptyList();
349                 for ( ReportPlugin plugin : reportPlugins )
350                 {
351                     String artifactId = plugin.getArtifactId();
352                     if ( "maven-jxr-plugin".equals( artifactId ) || "jxr-maven-plugin".equals( artifactId ) )
353                     {
354                         location = relativePath;
355                     }
356                 }
357             }
358 
359             if ( location == null )
360             {
361                 getLog().warn( "Unable to locate Source XRef to link to - DISABLED" );
362             }
363         }
364         return location;
365     }
366 
367     /**
368      * Convenience method to get the list of files where the PMD tool will be executed
369      *
370      * @return a List of the files where the PMD tool will be executed
371      * @throws IOException If an I/O error occurs during construction of the
372      *                     canonical pathnames of the files
373      */
374     protected Map<File, PmdFileInfo> getFilesToProcess()
375         throws IOException
376     {
377         if ( aggregate && !project.isExecutionRoot() )
378         {
379             return Collections.emptyMap();
380         }
381 
382         if ( excludeRoots == null )
383         {
384             excludeRoots = new File[0];
385         }
386 
387         Collection<File> excludeRootFiles = new HashSet<>( excludeRoots.length );
388 
389         for ( File file : excludeRoots )
390         {
391             if ( file.isDirectory() )
392             {
393                 excludeRootFiles.add( file );
394             }
395         }
396 
397         List<PmdFileInfo> directories = new ArrayList<>();
398 
399         if ( null == compileSourceRoots )
400         {
401             compileSourceRoots = project.getCompileSourceRoots();
402         }
403         if ( compileSourceRoots != null )
404         {
405             for ( String root : compileSourceRoots )
406             {
407                 File sroot = new File( root );
408                 if ( sroot.exists() )
409                 {
410                     String sourceXref = constructXRefLocation( false );
411                     directories.add( new PmdFileInfo( project, sroot, sourceXref ) );
412                 }
413             }
414         }
415 
416         if ( null == testSourceRoots )
417         {
418             testSourceRoots = project.getTestCompileSourceRoots();
419         }
420         if ( includeTests && testSourceRoots != null )
421         {
422             for ( String root : testSourceRoots )
423             {
424                 File sroot = new File( root );
425                 if ( sroot.exists() )
426                 {
427                     String testXref = constructXRefLocation( true );
428                     directories.add( new PmdFileInfo( project, sroot, testXref ) );
429                 }
430             }
431         }
432         if ( isAggregator() )
433         {
434             for ( MavenProject localProject : getAggregatedProjects() )
435             {
436                 List<String> localCompileSourceRoots = localProject.getCompileSourceRoots();
437                 for ( String root : localCompileSourceRoots )
438                 {
439                     File sroot = new File( root );
440                     if ( sroot.exists() )
441                     {
442                         String sourceXref = constructXRefLocation( false );
443                         directories.add( new PmdFileInfo( localProject, sroot, sourceXref ) );
444                     }
445                 }
446                 if ( includeTests )
447                 {
448                     List<String> localTestCompileSourceRoots = localProject.getTestCompileSourceRoots();
449                     for ( String root : localTestCompileSourceRoots )
450                     {
451                         File sroot = new File( root );
452                         if ( sroot.exists() )
453                         {
454                             String testXref = constructXRefLocation( true );
455                             directories.add( new PmdFileInfo( localProject, sroot, testXref ) );
456                         }
457                     }
458                 }
459             }
460 
461         }
462 
463         String excluding = getExcludes();
464         getLog().debug( "Exclusions: " + excluding );
465         String including = getIncludes();
466         getLog().debug( "Inclusions: " + including );
467 
468         Map<File, PmdFileInfo> files = new TreeMap<>();
469 
470         for ( PmdFileInfo finfo : directories )
471         {
472             getLog().debug( "Searching for files in directory " + finfo.getSourceDirectory().toString() );
473             File sourceDirectory = finfo.getSourceDirectory();
474             if ( sourceDirectory.isDirectory() && !isDirectoryExcluded( excludeRootFiles, sourceDirectory ) )
475             {
476                 List<File> newfiles = FileUtils.getFiles( sourceDirectory, including, excluding );
477                 for ( File newfile : newfiles )
478                 {
479                     files.put( newfile.getCanonicalFile(), finfo );
480                 }
481             }
482         }
483 
484         return files;
485     }
486 
487     private boolean isDirectoryExcluded( Collection<File> excludeRootFiles, File sourceDirectoryToCheck )
488     {
489         boolean returnVal = false;
490         for ( File excludeDir : excludeRootFiles )
491         {
492             try
493             {
494                 if ( sourceDirectoryToCheck.getCanonicalPath().startsWith( excludeDir.getCanonicalPath() ) )
495                 {
496                     getLog().debug( "Directory " + sourceDirectoryToCheck.getAbsolutePath()
497                                         + " has been excluded as it matches excludeRoot "
498                                         + excludeDir.getAbsolutePath() );
499                     returnVal = true;
500                     break;
501                 }
502             }
503             catch ( IOException e )
504             {
505                 getLog().warn( "Error while checking " + sourceDirectoryToCheck
506                                + " whether it should be excluded.", e );
507             }
508         }
509         return returnVal;
510     }
511 
512     /**
513      * Gets the comma separated list of effective include patterns.
514      *
515      * @return The comma separated list of effective include patterns, never <code>null</code>.
516      */
517     private String getIncludes()
518     {
519         Collection<String> patterns = new LinkedHashSet<>();
520         if ( includes != null )
521         {
522             patterns.addAll( includes );
523         }
524         if ( patterns.isEmpty() )
525         {
526             patterns.add( "**/*.java" );
527         }
528         return StringUtils.join( patterns.iterator(), "," );
529     }
530 
531     /**
532      * Gets the comma separated list of effective exclude patterns.
533      *
534      * @return The comma separated list of effective exclude patterns, never <code>null</code>.
535      */
536     private String getExcludes()
537     {
538         Collection<String> patterns = new LinkedHashSet<>( FileUtils.getDefaultExcludesAsList() );
539         if ( excludes != null )
540         {
541             patterns.addAll( excludes );
542         }
543         return StringUtils.join( patterns.iterator(), "," );
544     }
545 
546     protected boolean isXml()
547     {
548         return "xml".equals( format );
549     }
550 
551     /**
552      * {@inheritDoc}
553      */
554     @Override
555     public boolean canGenerateReport()
556     {
557         if ( aggregate && !project.isExecutionRoot() )
558         {
559             return false;
560         }
561 
562         if ( !isAggregator() && "pom".equalsIgnoreCase( project.getPackaging() ) )
563         {
564             return false;
565         }
566 
567         // if format is XML, we need to output it even if the file list is empty
568         // so the "check" goals can check for failures
569         if ( isXml() )
570         {
571             return true;
572         }
573         try
574         {
575             filesToProcess = getFilesToProcess();
576             if ( filesToProcess.isEmpty() )
577             {
578                 return false;
579             }
580         }
581         catch ( IOException e )
582         {
583             getLog().error( e );
584         }
585         return true;
586     }
587 
588     /**
589      * {@inheritDoc}
590      */
591     @Override
592     protected String getOutputDirectory()
593     {
594         return outputDirectory.getAbsolutePath();
595     }
596 
597     protected String getSourceEncoding()
598     {
599         return sourceEncoding;
600     }
601 
602     /**
603      * Gets the effective reporting output files encoding.
604      *
605      * @return The effective reporting output file encoding, never <code>null</code>.
606      * @since 2.5
607      */
608     @Override
609     protected String getOutputEncoding()
610     {
611         return ( outputEncoding != null ) ? outputEncoding : ReaderFactory.UTF_8;
612     }
613 
614     protected String determineCurrentRootLogLevel()
615     {
616         String logLevel = System.getProperty( "org.slf4j.simpleLogger.defaultLogLevel" );
617         if ( logLevel == null )
618         {
619             logLevel = System.getProperty( "maven.logging.root.level" );
620         }
621         if ( logLevel == null )
622         {
623             // TODO: logback level
624             logLevel = "info";
625         }
626         return logLevel;
627     }
628 
629     static String getPmdVersion()
630     {
631         return PMDVersion.VERSION;
632     }
633 
634     //TODO remove the part with ToolchainManager lookup once we depend on
635     //3.0.9 (have it as prerequisite). Define as regular component field then.
636     protected final Toolchain getToolchain()
637     {
638         Toolchain tc = null;
639 
640         if ( jdkToolchain != null )
641         {
642             // Maven 3.3.1 has plugin execution scoped Toolchain Support
643             try
644             {
645                 Method getToolchainsMethod =
646                     toolchainManager.getClass().getMethod( "getToolchains", MavenSession.class, String.class,
647                                                            Map.class );
648 
649                 @SuppressWarnings( "unchecked" )
650                 List<Toolchain> tcs =
651                     (List<Toolchain>) getToolchainsMethod.invoke( toolchainManager, session, "jdk",
652                                                                   jdkToolchain );
653 
654                 if ( tcs != null && !tcs.isEmpty() )
655                 {
656                     tc = tcs.get( 0 );
657                 }
658             }
659             catch ( NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
660                 | InvocationTargetException e )
661             {
662                 // ignore
663             }
664         }
665 
666         if ( tc == null )
667         {
668             tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
669         }
670 
671         return tc;
672     }
673 
674     protected boolean isAggregator()
675     {
676         // returning here aggregate for backwards compatibility
677         return aggregate;
678     }
679 
680     // Note: same logic as in m-javadoc-p (MJAVADOC-134)
681     protected Collection<MavenProject> getAggregatedProjects()
682     {
683         Map<Path, MavenProject> reactorProjectsMap = new HashMap<>();
684         for ( MavenProject reactorProject : this.reactorProjects )
685         {
686             reactorProjectsMap.put( reactorProject.getBasedir().toPath(), reactorProject );
687         }
688 
689         return modulesForAggregatedProject( project, reactorProjectsMap );
690     }
691 
692     /**
693      * Recursively add the modules of the aggregatedProject to the set of aggregatedModules.
694      * 
695      * @param aggregatedProject the project being aggregated
696      * @param reactorProjectsMap map of (still) available reactor projects 
697      * @throws MavenReportException if any
698      */
699     private Set<MavenProject> modulesForAggregatedProject( MavenProject aggregatedProject,
700                                                            Map<Path, MavenProject> reactorProjectsMap )
701     {
702         // Maven does not supply an easy way to get the projects representing
703         // the modules of a project. So we will get the paths to the base
704         // directories of the modules from the project and compare with the
705         // base directories of the projects in the reactor.
706 
707         if ( aggregatedProject.getModules().isEmpty() )
708         {
709             return Collections.singleton( aggregatedProject );
710         }
711 
712         List<Path> modulePaths = new LinkedList<Path>();
713         for ( String module :  aggregatedProject.getModules() )
714         {
715             modulePaths.add( new File( aggregatedProject.getBasedir(), module ).toPath() );
716         }
717 
718         Set<MavenProject> aggregatedModules = new LinkedHashSet<>();
719 
720         for ( Path modulePath : modulePaths )
721         {
722             MavenProject module = reactorProjectsMap.remove( modulePath );
723             if ( module != null )
724             {
725                 aggregatedModules.addAll( modulesForAggregatedProject( module, reactorProjectsMap ) );
726             }
727         }
728 
729         return aggregatedModules;
730     }
731 }