View Javadoc
1   package org.apache.maven.plugin.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.FileNotFoundException;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStreamWriter;
27  import java.io.PrintStream;
28  import java.io.Writer;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Properties;
35  import java.util.ResourceBundle;
36  
37  import org.apache.maven.doxia.sink.Sink;
38  import org.apache.maven.plugin.MojoExecutionException;
39  import org.apache.maven.plugins.annotations.Component;
40  import org.apache.maven.plugins.annotations.Mojo;
41  import org.apache.maven.plugins.annotations.Parameter;
42  import org.apache.maven.plugins.annotations.ResolutionScope;
43  import org.apache.maven.reporting.MavenReportException;
44  import org.codehaus.plexus.resource.ResourceManager;
45  import org.codehaus.plexus.resource.loader.FileResourceCreationException;
46  import org.codehaus.plexus.resource.loader.FileResourceLoader;
47  import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
48  import org.codehaus.plexus.util.FileUtils;
49  import org.codehaus.plexus.util.ReaderFactory;
50  import org.codehaus.plexus.util.StringUtils;
51  
52  import net.sourceforge.pmd.PMD;
53  import net.sourceforge.pmd.PMDConfiguration;
54  import net.sourceforge.pmd.Report;
55  import net.sourceforge.pmd.RuleContext;
56  import net.sourceforge.pmd.RulePriority;
57  import net.sourceforge.pmd.RuleSetFactory;
58  import net.sourceforge.pmd.RuleSetReferenceId;
59  import net.sourceforge.pmd.RuleViolation;
60  import net.sourceforge.pmd.benchmark.Benchmarker;
61  import net.sourceforge.pmd.benchmark.TextReport;
62  import net.sourceforge.pmd.lang.LanguageRegistry;
63  import net.sourceforge.pmd.lang.LanguageVersion;
64  import net.sourceforge.pmd.renderers.CSVRenderer;
65  import net.sourceforge.pmd.renderers.HTMLRenderer;
66  import net.sourceforge.pmd.renderers.Renderer;
67  import net.sourceforge.pmd.renderers.TextRenderer;
68  import net.sourceforge.pmd.renderers.XMLRenderer;
69  import net.sourceforge.pmd.util.datasource.DataSource;
70  import net.sourceforge.pmd.util.datasource.FileDataSource;
71  
72  /**
73   * Creates a PMD report.
74   *
75   * @author Brett Porter
76   * @version $Id: PmdReport.html 1011460 2017-05-01 07:24:19Z adangel $
77   * @since 2.0
78   */
79  @Mojo( name = "pmd", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST )
80  public class PmdReport
81      extends AbstractPmdReport
82  {
83      /**
84       * The target JDK to analyze based on. Should match the target used in the compiler plugin. Valid values are
85       * currently <code>1.3</code>, <code>1.4</code>, <code>1.5</code>, <code>1.6</code>, <code>1.7</code> and
86       * <code>1.8</code>.
87       * <p>
88       *   <b>Note:</b> this parameter is only used if the language parameter is set to <code>java</code>.
89       * </p>
90       */
91      @Parameter( property = "targetJdk", defaultValue = "${maven.compiler.target}" )
92      private String targetJdk;
93  
94      /**
95       * The programming language to be analyzed by PMD. Valid values are currently <code>java</code>,
96       * <code>javascript</code> and <code>jsp</code>.
97       *
98       * @since 3.0
99       */
100     @Parameter( defaultValue = "java" )
101     private String language;
102 
103     /**
104      * The rule priority threshold; rules with lower priority than this will not be evaluated.
105      *
106      * @since 2.1
107      */
108     @Parameter( property = "minimumPriority", defaultValue = "5" )
109     private int minimumPriority = 5;
110 
111     /**
112      * Skip the PMD report generation. Most useful on the command line via "-Dpmd.skip=true".
113      *
114      * @since 2.1
115      */
116     @Parameter( property = "pmd.skip", defaultValue = "false" )
117     private boolean skip;
118 
119     /**
120      * The PMD rulesets to use. See the
121      * <a href="http://pmd.github.io/pmd-5.5.1/pmd-java/rules/index.html">Stock Java Rulesets</a> for a
122      * list of some included. Defaults to the java-basic, java-empty, java-imports, java-unnecessary
123      * and java-unusedcode rulesets.
124      */
125     @Parameter
126     private String[] rulesets = new String[] { "java-basic", "java-empty", "java-imports",
127             "java-unnecessary", "java-unusedcode" };
128 
129     /**
130      * Controls whether the project's compile/test classpath should be passed to PMD to enable its type resolution
131      * feature.
132      *
133      * @since 3.0
134      */
135     @Parameter( property = "pmd.typeResolution", defaultValue = "true" )
136     private boolean typeResolution;
137 
138     /**
139      * Controls whether PMD will track benchmark information.
140      *
141      * @since 3.1
142      */
143     @Parameter( property = "pmd.benchmark", defaultValue = "false" )
144     private boolean benchmark;
145 
146     /**
147      * Benchmark output filename.
148      *
149      * @since 3.1
150      */
151     @Parameter( property = "pmd.benchmarkOutputFilename",
152                     defaultValue = "${project.build.directory}/pmd-benchmark.txt" )
153     private String benchmarkOutputFilename;
154 
155     /**
156      * Source level marker used to indicate whether a RuleViolation should be suppressed. If it is not set, PMD's
157      * default will be used, which is <code>NOPMD</code>. See also <a
158      * href="https://pmd.github.io/latest/usage/suppressing.html">PMD &#x2013; Suppressing warnings</a>.
159      *
160      * @since 3.4
161      */
162     @Parameter( property = "pmd.suppressMarker" )
163     private String suppressMarker;
164 
165     /**
166      */
167     @Component
168     private ResourceManager locator;
169 
170     /** The PMD renderer for collecting violations. */
171     private PmdCollectingRenderer renderer;
172 
173     /** Helper to exclude violations given as a properties file. */
174     private final ExcludeViolationsFromFile excludeFromFile = new ExcludeViolationsFromFile();
175 
176     /**
177      * per default pmd executions error are ignored to not break the whole
178      *
179      * @since 3.1
180      */
181     @Parameter( property = "pmd.skipPmdError", defaultValue = "true" )
182     private boolean skipPmdError;
183 
184     /**
185      * Enables the analysis cache, which speeds up PMD. This
186      * requires a cache file, that contains the results of the last
187      * PMD run. Thus the cache is only effective, if this file is
188      * not cleaned between runs.
189      *
190      * @since 3.8
191      */
192     @Parameter( property = "pmd.analysisCache", defaultValue = "false" )
193     private boolean analysisCache;
194 
195     /**
196      * The location of the analysis cache, if it is enabled.
197      * This file contains the results of the last PMD run and must not be cleaned
198      * between consecutive PMD runs. Otherwise the cache is not in use.
199      * If the file doesn't exist, PMD executes as if there is no cache enabled and
200      * all files are analyzed. Otherwise only changed files will be analyzed again.
201      *
202      * @since 3.8
203      */
204     @Parameter( property = "pmd.analysisCacheLocation", defaultValue = "${project.build.directory}/pmd/pmd.cache" )
205     private String analysisCacheLocation;
206 
207     /**
208      * {@inheritDoc}
209      */
210     public String getName( Locale locale )
211     {
212         return getBundle( locale ).getString( "report.pmd.name" );
213     }
214 
215     /**
216      * {@inheritDoc}
217      */
218     public String getDescription( Locale locale )
219     {
220         return getBundle( locale ).getString( "report.pmd.description" );
221     }
222 
223     public void setRulesets( String[] rules )
224     {
225         rulesets = rules;
226     }
227 
228     /**
229      * {@inheritDoc}
230      */
231     @Override
232     public void executeReport( Locale locale )
233         throws MavenReportException
234     {
235         try
236         {
237             execute( locale );
238         }
239         finally
240         {
241             if ( getSink() != null )
242             {
243                 getSink().close();
244             }
245         }
246     }
247 
248     private void execute( Locale locale )
249         throws MavenReportException
250     {
251         if ( !skip && canGenerateReport() )
252         {
253             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
254             try
255             {
256                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
257 
258                 Report report = generateReport( locale );
259 
260                 if ( !isHtml() && !isXml() )
261                 {
262                     writeNonHtml( report );
263                 }
264             }
265             finally
266             {
267                 Thread.currentThread().setContextClassLoader( origLoader );
268             }
269         }
270     }
271 
272     @Override
273     public boolean canGenerateReport()
274     {
275         if ( skip )
276         {
277             return false;
278         }
279 
280         boolean result = super.canGenerateReport();
281         if ( result )
282         {
283             try
284             {
285                 executePmdWithClassloader();
286                 if ( skipEmptyReport )
287                 {
288                     result = renderer.hasViolations();
289                     if ( result )
290                     {
291                         getLog().debug( "Skipping report since skipEmptyReport is true and"
292                                             + "there are no PMD violations." );
293                     }
294                 }
295             }
296             catch ( MavenReportException e )
297             {
298                 throw new RuntimeException( e );
299             }
300         }
301         return result;
302     }
303 
304     private void executePmdWithClassloader()
305         throws MavenReportException
306     {
307         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
308         try
309         {
310             Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
311             executePmd();
312         }
313         finally
314         {
315             Thread.currentThread().setContextClassLoader( origLoader );
316         }
317     }
318 
319     private void executePmd()
320         throws MavenReportException
321     {
322         if ( renderer != null )
323         {
324             // PMD has already been run
325             getLog().debug( "PMD has already been run - skipping redundant execution." );
326             return;
327         }
328 
329         try
330         {
331             excludeFromFile.loadExcludeFromFailuresData( excludeFromFailureFile );
332         }
333         catch ( MojoExecutionException e )
334         {
335             throw new MavenReportException( "Unable to load exclusions", e );
336         }
337 
338         // configure ResourceManager
339         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
340         locator.addSearchPath( "url", "" );
341         locator.setOutputDirectory( targetDirectory );
342 
343         renderer = new PmdCollectingRenderer();
344         PMDConfiguration pmdConfiguration = getPMDConfiguration();
345 
346         String[] sets = new String[rulesets.length];
347         try
348         {
349             for ( int idx = 0; idx < rulesets.length; idx++ )
350             {
351                 String set = rulesets[idx];
352                 getLog().debug( "Preparing ruleset: " + set );
353                 RuleSetReferenceId id = new RuleSetReferenceId( set );
354                 File ruleset = locator.getResourceAsFile( id.getRuleSetFileName(), getLocationTemp( set ) );
355                 if ( null == ruleset )
356                 {
357                     throw new MavenReportException( "Could not resolve " + set );
358                 }
359                 sets[idx] = ruleset.getAbsolutePath();
360             }
361         }
362         catch ( ResourceNotFoundException e )
363         {
364             throw new MavenReportException( e.getMessage(), e );
365         }
366         catch ( FileResourceCreationException e )
367         {
368             throw new MavenReportException( e.getMessage(), e );
369         }
370         pmdConfiguration.setRuleSets( StringUtils.join( sets, "," ) );
371 
372         try
373         {
374             if ( filesToProcess == null )
375             {
376                 filesToProcess = getFilesToProcess();
377             }
378 
379             if ( filesToProcess.isEmpty() && !"java".equals( language ) )
380             {
381                 getLog().warn( "No files found to process. Did you add your additional source folders like javascript?"
382                                    + " (see also build-helper-maven-plugin)" );
383             }
384         }
385         catch ( IOException e )
386         {
387             throw new MavenReportException( "Can't get file list", e );
388         }
389 
390         String encoding = getSourceEncoding();
391         if ( StringUtils.isEmpty( encoding ) && !filesToProcess.isEmpty() )
392         {
393             getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
394                                + ", i.e. build is platform dependent!" );
395             encoding = ReaderFactory.FILE_ENCODING;
396         }
397         pmdConfiguration.setSourceEncoding( encoding );
398 
399         List<DataSource> dataSources = new ArrayList<>( filesToProcess.size() );
400         for ( File f : filesToProcess.keySet() )
401         {
402             dataSources.add( new FileDataSource( f ) );
403         }
404 
405         if ( sets.length > 0 )
406         {
407             processFilesWithPMD( pmdConfiguration, dataSources );
408         }
409         else
410         {
411             getLog().debug( "Skipping PMD execution as no rulesets are defined." );
412         }
413 
414         if ( renderer.hasErrors() )
415         {
416             if ( !skipPmdError )
417             {
418                 getLog().error( "PMD processing errors:" );
419                 getLog().error( renderer.getErrorsAsString() );
420                 throw new MavenReportException( "Found " + renderer.getErrors().size() + " PMD processing errors" );
421             }
422             getLog().warn( "There are " + renderer.getErrors().size() + " PMD processing errors:" );
423             getLog().warn( renderer.getErrorsAsString() );
424         }
425 
426         removeExcludedViolations( renderer.getViolations() );
427 
428         // if format is XML, we need to output it even if the file list is empty or we have no violations
429         // so the "check" goals can check for violations
430         if ( isXml() && renderer != null )
431         {
432             writeNonHtml( renderer.asReport() );
433         }
434 
435         if ( benchmark )
436         {
437             try ( PrintStream benchmarkFileStream = new PrintStream( benchmarkOutputFilename ) )
438             {
439                 ( new TextReport() ).generate( Benchmarker.values(), benchmarkFileStream );
440             }
441             catch ( FileNotFoundException fnfe )
442             {
443                 getLog().error( "Unable to generate benchmark file: " + benchmarkOutputFilename, fnfe );
444             }
445         }
446     }
447 
448     private void removeExcludedViolations( List<RuleViolation> violations )
449     {
450         getLog().debug( "Removing excluded violations. Using " + excludeFromFile.countExclusions()
451             + " configured exclusions." );
452         int violationsBefore = violations.size();
453 
454         Iterator<RuleViolation> iterator = violations.iterator();
455         while ( iterator.hasNext() )
456         {
457             RuleViolation rv = iterator.next();
458             if ( excludeFromFile.isExcludedFromFailure( rv ) )
459             {
460                 iterator.remove();
461             }
462         }
463 
464         int numberOfExcludedViolations = violationsBefore - violations.size();
465         getLog().debug( "Excluded " + numberOfExcludedViolations + " violations." );
466     }
467 
468     private void processFilesWithPMD( PMDConfiguration pmdConfiguration, List<DataSource> dataSources )
469             throws MavenReportException
470     {
471         RuleSetFactory ruleSetFactory = new RuleSetFactory( RuleSetFactory.class.getClassLoader(),
472                 RulePriority.valueOf( this.minimumPriority ), false, true );
473         RuleContext ruleContext = new RuleContext();
474 
475         try
476         {
477             getLog().debug( "Executing PMD..." );
478             PMD.processFiles( pmdConfiguration, ruleSetFactory, dataSources, ruleContext,
479                               Arrays.<Renderer>asList( renderer ) );
480 
481             if ( getLog().isDebugEnabled() )
482             {
483                 getLog().debug( "PMD finished. Found " + renderer.getViolations().size() + " violations." );
484             }
485         }
486         catch ( Exception e )
487         {
488             String message = "Failure executing PMD: " + e.getLocalizedMessage();
489             if ( !skipPmdError )
490             {
491                 throw new MavenReportException( message, e );
492             }
493             getLog().warn( message, e );
494         }
495     }
496 
497     private Report generateReport( Locale locale )
498         throws MavenReportException
499     {
500         Sink sink = getSink();
501         PmdReportGenerator doxiaRenderer = new PmdReportGenerator( getLog(), sink, getBundle( locale ), aggregate );
502         doxiaRenderer.setFiles( filesToProcess );
503         doxiaRenderer.setViolations( renderer.getViolations() );
504 
505         try
506         {
507             doxiaRenderer.beginDocument();
508             doxiaRenderer.render();
509             doxiaRenderer.endDocument();
510         }
511         catch ( IOException e )
512         {
513             getLog().warn( "Failure creating the report: " + e.getLocalizedMessage(), e );
514         }
515 
516         return renderer.asReport();
517     }
518 
519     /**
520      * Convenience method to get the location of the specified file name.
521      *
522      * @param name the name of the file whose location is to be resolved
523      * @return a String that contains the absolute file name of the file
524      */
525     protected String getLocationTemp( String name )
526     {
527         String loc = name;
528         if ( loc.indexOf( '/' ) != -1 )
529         {
530             loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
531         }
532         if ( loc.indexOf( '\\' ) != -1 )
533         {
534             loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
535         }
536 
537         // MPMD-127 in the case that the rules are defined externally on a url
538         // we need to replace some special url characters that cannot be
539         // used in filenames on disk or produce ackward filenames.
540         // replace all occurrences of the following characters: ? : & = %
541         loc = loc.replaceAll( "[\\?\\:\\&\\=\\%]", "_" );
542 
543         if ( !loc.endsWith( ".xml" ) )
544         {
545             loc = loc + ".xml";
546         }
547 
548         getLog().debug( "Before: " + name + " After: " + loc );
549         return loc;
550     }
551 
552     /**
553      * Use the PMD renderers to render in any format aside from HTML.
554      *
555      * @param report
556      * @throws MavenReportException
557      */
558     private void writeNonHtml( Report report )
559         throws MavenReportException
560     {
561         Renderer r = createRenderer();
562 
563         if ( r == null )
564         {
565             return;
566         }
567 
568         File targetFile = new File( targetDirectory, "pmd." + format );
569         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) )
570         {
571             targetDirectory.mkdirs();
572 
573             r.setWriter( writer );
574             r.start();
575             r.renderFileReport( report );
576             r.end();
577 
578             if ( includeXmlInSite )
579             {
580                 File siteDir = getReportOutputDirectory();
581                 siteDir.mkdirs();
582                 FileUtils.copyFile( targetFile, new File( siteDir, "pmd." + format ) );
583             }
584         }
585         catch ( IOException ioe )
586         {
587             throw new MavenReportException( ioe.getMessage(), ioe );
588         }
589     }
590 
591     /**
592      * Constructs the PMD configuration class, passing it an argument that configures the target JDK.
593      *
594      * @return the resulting PMD
595      * @throws org.apache.maven.reporting.MavenReportException if targetJdk is not supported
596      */
597     public PMDConfiguration getPMDConfiguration()
598         throws MavenReportException
599     {
600         PMDConfiguration configuration = new PMDConfiguration();
601         LanguageVersion languageVersion = null;
602 
603         if ( ( "java".equals( language ) || null == language ) && null != targetJdk )
604         {
605             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "java " + targetJdk );
606             if ( languageVersion == null )
607             {
608                 throw new MavenReportException( "Unsupported targetJdk value '" + targetJdk + "'." );
609             }
610         }
611         else if ( "javascript".equals( language ) || "ecmascript".equals( language ) )
612         {
613             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "ecmascript" );
614         }
615         else if ( "jsp".equals( language ) )
616         {
617             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "jsp" );
618         }
619 
620         if ( languageVersion != null )
621         {
622             getLog().debug( "Using language " + languageVersion );
623             configuration.setDefaultLanguageVersion( languageVersion );
624         }
625 
626         if ( typeResolution )
627         {
628             try
629             {
630                 @SuppressWarnings( "unchecked" )
631                 List<String> classpath =
632                     includeTests ? project.getTestClasspathElements() : project.getCompileClasspathElements();
633                 getLog().debug( "Using aux classpath: " + classpath );
634                 configuration.prependClasspath( StringUtils.join( classpath.iterator(), File.pathSeparator ) );
635             }
636             catch ( Exception e )
637             {
638                 throw new MavenReportException( e.getMessage(), e );
639             }
640         }
641 
642         if ( null != suppressMarker )
643         {
644             configuration.setSuppressMarker( suppressMarker );
645         }
646 
647         configuration.setBenchmark( benchmark );
648 
649         if ( analysisCache )
650         {
651             configuration.setAnalysisCacheLocation( analysisCacheLocation );
652             getLog().debug( "Using analysis cache location: " + analysisCacheLocation );
653         }
654 
655         return configuration;
656     }
657 
658     /**
659      * {@inheritDoc}
660      */
661     public String getOutputName()
662     {
663         return "pmd";
664     }
665 
666     private static ResourceBundle getBundle( Locale locale )
667     {
668         return ResourceBundle.getBundle( "pmd-report", locale, PmdReport.class.getClassLoader() );
669     }
670 
671     /**
672      * Create and return the correct renderer for the output type.
673      *
674      * @return the renderer based on the configured output
675      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
676      */
677     public final Renderer createRenderer()
678         throws MavenReportException
679     {
680         Renderer result = null;
681         if ( "xml".equals( format ) )
682         {
683             result = new XMLRenderer( getOutputEncoding() );
684         }
685         else if ( "txt".equals( format ) )
686         {
687             result = new TextRenderer();
688         }
689         else if ( "csv".equals( format ) )
690         {
691             result = new CSVRenderer();
692         }
693         else if ( "html".equals( format ) )
694         {
695             result = new HTMLRenderer();
696         }
697         else if ( !"".equals( format ) && !"none".equals( format ) )
698         {
699             try
700             {
701                 result = (Renderer) Class.forName( format ).getConstructor( Properties.class ).
702                                 newInstance( new Properties() );
703             }
704             catch ( Exception e )
705             {
706                 throw new MavenReportException( "Can't find PMD custom format " + format + ": "
707                     + e.getClass().getName(), e );
708             }
709         }
710 
711         return result;
712     }
713 }