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