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