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 999063 2016-10-08 16:53:12Z 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 = "false" )
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      * {@inheritDoc}
186      */
187     public String getName( Locale locale )
188     {
189         return getBundle( locale ).getString( "report.pmd.name" );
190     }
191 
192     /**
193      * {@inheritDoc}
194      */
195     public String getDescription( Locale locale )
196     {
197         return getBundle( locale ).getString( "report.pmd.description" );
198     }
199 
200     public void setRulesets( String[] rules )
201     {
202         rulesets = rules;
203     }
204 
205     /**
206      * {@inheritDoc}
207      */
208     @Override
209     public void executeReport( Locale locale )
210         throws MavenReportException
211     {
212         try
213         {
214             execute( locale );
215         }
216         finally
217         {
218             if ( getSink() != null )
219             {
220                 getSink().close();
221             }
222         }
223     }
224 
225     private void execute( Locale locale )
226         throws MavenReportException
227     {
228         if ( !skip && canGenerateReport() )
229         {
230             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
231             try
232             {
233                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
234 
235                 Report report = generateReport( locale );
236 
237                 if ( !isHtml() && !isXml() )
238                 {
239                     writeNonHtml( report );
240                 }
241             }
242             finally
243             {
244                 Thread.currentThread().setContextClassLoader( origLoader );
245             }
246         }
247     }
248 
249     @Override
250     public boolean canGenerateReport()
251     {
252         if ( skip )
253         {
254             return false;
255         }
256 
257         boolean result = super.canGenerateReport();
258         if ( result )
259         {
260             try
261             {
262                 executePmdWithClassloader();
263                 if ( skipEmptyReport )
264                 {
265                     result = renderer.hasViolations();
266                     if ( result )
267                     {
268                         getLog().debug( "Skipping report since skipEmptyReport is true and"
269                                             + "there are no PMD violations." );
270                     }
271                 }
272             }
273             catch ( MavenReportException e )
274             {
275                 throw new RuntimeException( e );
276             }
277         }
278         return result;
279     }
280 
281     private void executePmdWithClassloader()
282         throws MavenReportException
283     {
284         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
285         try
286         {
287             Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
288             executePmd();
289         }
290         finally
291         {
292             Thread.currentThread().setContextClassLoader( origLoader );
293         }
294     }
295 
296     private void executePmd()
297         throws MavenReportException
298     {
299         if ( renderer != null )
300         {
301             // PMD has already been run
302             getLog().debug( "PMD has already been run - skipping redundant execution." );
303             return;
304         }
305 
306         try
307         {
308             excludeFromFile.loadExcludeFromFailuresData( excludeFromFailureFile );
309         }
310         catch ( MojoExecutionException e )
311         {
312             throw new MavenReportException( "Unable to load exclusions", e );
313         }
314 
315         // configure ResourceManager
316         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
317         locator.addSearchPath( "url", "" );
318         locator.setOutputDirectory( targetDirectory );
319 
320         renderer = new PmdCollectingRenderer();
321         PMDConfiguration pmdConfiguration = getPMDConfiguration();
322 
323         String[] sets = new String[rulesets.length];
324         try
325         {
326             for ( int idx = 0; idx < rulesets.length; idx++ )
327             {
328                 String set = rulesets[idx];
329                 getLog().debug( "Preparing ruleset: " + set );
330                 RuleSetReferenceId id = new RuleSetReferenceId( set );
331                 File ruleset = locator.getResourceAsFile( id.getRuleSetFileName(), getLocationTemp( set ) );
332                 if ( null == ruleset )
333                 {
334                     throw new MavenReportException( "Could not resolve " + set );
335                 }
336                 sets[idx] = ruleset.getAbsolutePath();
337             }
338         }
339         catch ( ResourceNotFoundException e )
340         {
341             throw new MavenReportException( e.getMessage(), e );
342         }
343         catch ( FileResourceCreationException e )
344         {
345             throw new MavenReportException( e.getMessage(), e );
346         }
347         pmdConfiguration.setRuleSets( StringUtils.join( sets, "," ) );
348 
349         try
350         {
351             if ( filesToProcess == null )
352             {
353                 filesToProcess = getFilesToProcess();
354             }
355 
356             if ( filesToProcess.isEmpty() && !"java".equals( language ) )
357             {
358                 getLog().warn( "No files found to process. Did you add your additional source folders like javascript?"
359                                    + " (see also build-helper-maven-plugin)" );
360             }
361         }
362         catch ( IOException e )
363         {
364             throw new MavenReportException( "Can't get file list", e );
365         }
366 
367         String encoding = getSourceEncoding();
368         if ( StringUtils.isEmpty( encoding ) && !filesToProcess.isEmpty() )
369         {
370             getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
371                                + ", i.e. build is platform dependent!" );
372             encoding = ReaderFactory.FILE_ENCODING;
373         }
374         pmdConfiguration.setSourceEncoding( encoding );
375 
376         List<DataSource> dataSources = new ArrayList<>( filesToProcess.size() );
377         for ( File f : filesToProcess.keySet() )
378         {
379             dataSources.add( new FileDataSource( f ) );
380         }
381 
382         if ( sets.length > 0 )
383         {
384             processFilesWithPMD( pmdConfiguration, dataSources );
385         }
386         else
387         {
388             getLog().debug( "Skipping PMD execution as no rulesets are defined." );
389         }
390 
391         if ( renderer.hasErrors() )
392         {
393             if ( !skipPmdError )
394             {
395                 getLog().error( "PMD processing errors:" );
396                 getLog().error( renderer.getErrorsAsString() );
397                 throw new MavenReportException( "Found " + renderer.getErrors().size() + " PMD processing errors" );
398             }
399             getLog().warn( "There are " + renderer.getErrors().size() + " PMD processing errors:" );
400             getLog().warn( renderer.getErrorsAsString() );
401         }
402 
403         removeExcludedViolations( renderer.getViolations() );
404 
405         // if format is XML, we need to output it even if the file list is empty or we have no violations
406         // so the "check" goals can check for violations
407         if ( isXml() && renderer != null )
408         {
409             writeNonHtml( renderer.asReport() );
410         }
411 
412         if ( benchmark )
413         {
414             try ( PrintStream benchmarkFileStream = new PrintStream( benchmarkOutputFilename ) )
415             {
416                 ( new TextReport() ).generate( Benchmarker.values(), benchmarkFileStream );
417             }
418             catch ( FileNotFoundException fnfe )
419             {
420                 getLog().error( "Unable to generate benchmark file: " + benchmarkOutputFilename, fnfe );
421             }
422         }
423     }
424 
425     private void removeExcludedViolations( List<RuleViolation> violations )
426     {
427         getLog().debug( "Removing excluded violations. Using " + excludeFromFile.countExclusions()
428             + " configured exclusions." );
429         int violationsBefore = violations.size();
430 
431         Iterator<RuleViolation> iterator = violations.iterator();
432         while ( iterator.hasNext() )
433         {
434             RuleViolation rv = iterator.next();
435             if ( excludeFromFile.isExcludedFromFailure( rv ) )
436             {
437                 iterator.remove();
438             }
439         }
440 
441         int numberOfExcludedViolations = violationsBefore - violations.size();
442         getLog().debug( "Excluded " + numberOfExcludedViolations + " violations." );
443     }
444 
445     private void processFilesWithPMD( PMDConfiguration pmdConfiguration, List<DataSource> dataSources )
446             throws MavenReportException
447     {
448         RuleSetFactory ruleSetFactory = new RuleSetFactory();
449         ruleSetFactory.setMinimumPriority( RulePriority.valueOf( this.minimumPriority ) );
450         RuleContext ruleContext = new RuleContext();
451 
452         try
453         {
454             getLog().debug( "Executing PMD..." );
455             PMD.processFiles( pmdConfiguration, ruleSetFactory, dataSources, ruleContext,
456                               Arrays.<Renderer>asList( renderer ) );
457 
458             if ( getLog().isDebugEnabled() )
459             {
460                 getLog().debug( "PMD finished. Found " + renderer.getViolations().size() + " violations." );
461             }
462         }
463         catch ( Exception e )
464         {
465             String message = "Failure executing PMD: " + e.getLocalizedMessage();
466             if ( !skipPmdError )
467             {
468                 throw new MavenReportException( message, e );
469             }
470             getLog().warn( message, e );
471         }
472     }
473 
474     private Report generateReport( Locale locale )
475         throws MavenReportException
476     {
477         Sink sink = getSink();
478         PmdReportGenerator doxiaRenderer = new PmdReportGenerator( getLog(), sink, getBundle( locale ), aggregate );
479         doxiaRenderer.setFiles( filesToProcess );
480         doxiaRenderer.setViolations( renderer.getViolations() );
481 
482         try
483         {
484             doxiaRenderer.beginDocument();
485             doxiaRenderer.render();
486             doxiaRenderer.endDocument();
487         }
488         catch ( IOException e )
489         {
490             getLog().warn( "Failure creating the report: " + e.getLocalizedMessage(), e );
491         }
492 
493         return renderer.asReport();
494     }
495 
496     /**
497      * Convenience method to get the location of the specified file name.
498      *
499      * @param name the name of the file whose location is to be resolved
500      * @return a String that contains the absolute file name of the file
501      */
502     protected String getLocationTemp( String name )
503     {
504         String loc = name;
505         if ( loc.indexOf( '/' ) != -1 )
506         {
507             loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
508         }
509         if ( loc.indexOf( '\\' ) != -1 )
510         {
511             loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
512         }
513 
514         // MPMD-127 in the case that the rules are defined externally on a url
515         // we need to replace some special url characters that cannot be
516         // used in filenames on disk or produce ackward filenames.
517         // replace all occurrences of the following characters: ? : & = %
518         loc = loc.replaceAll( "[\\?\\:\\&\\=\\%]", "_" );
519 
520         if ( !loc.endsWith( ".xml" ) )
521         {
522             loc = loc + ".xml";
523         }
524 
525         getLog().debug( "Before: " + name + " After: " + loc );
526         return loc;
527     }
528 
529     /**
530      * Use the PMD renderers to render in any format aside from HTML.
531      *
532      * @param report
533      * @throws MavenReportException
534      */
535     private void writeNonHtml( Report report )
536         throws MavenReportException
537     {
538         Renderer r = createRenderer();
539 
540         if ( r == null )
541         {
542             return;
543         }
544 
545         File targetFile = new File( targetDirectory, "pmd." + format );
546         try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) )
547         {
548             targetDirectory.mkdirs();
549 
550             r.setWriter( writer );
551             r.start();
552             r.renderFileReport( report );
553             r.end();
554 
555             if ( includeXmlInSite )
556             {
557                 File siteDir = getReportOutputDirectory();
558                 siteDir.mkdirs();
559                 FileUtils.copyFile( targetFile, new File( siteDir, "pmd." + format ) );
560             }
561         }
562         catch ( IOException ioe )
563         {
564             throw new MavenReportException( ioe.getMessage(), ioe );
565         }
566     }
567 
568     /**
569      * Constructs the PMD configuration class, passing it an argument that configures the target JDK.
570      *
571      * @return the resulting PMD
572      * @throws org.apache.maven.reporting.MavenReportException if targetJdk is not supported
573      */
574     public PMDConfiguration getPMDConfiguration()
575         throws MavenReportException
576     {
577         PMDConfiguration configuration = new PMDConfiguration();
578         LanguageVersion languageVersion = null;
579 
580         if ( ( "java".equals( language ) || null == language ) && null != targetJdk )
581         {
582             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "java " + targetJdk );
583             if ( languageVersion == null )
584             {
585                 throw new MavenReportException( "Unsupported targetJdk value '" + targetJdk + "'." );
586             }
587         }
588         else if ( "javascript".equals( language ) || "ecmascript".equals( language ) )
589         {
590             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "ecmascript" );
591         }
592         else if ( "jsp".equals( language ) )
593         {
594             languageVersion = LanguageRegistry.findLanguageVersionByTerseName( "jsp" );
595         }
596 
597         if ( languageVersion != null )
598         {
599             getLog().debug( "Using language " + languageVersion );
600             configuration.setDefaultLanguageVersion( languageVersion );
601         }
602 
603         if ( typeResolution )
604         {
605             try
606             {
607                 @SuppressWarnings( "unchecked" )
608                 List<String> classpath =
609                     includeTests ? project.getTestClasspathElements() : project.getCompileClasspathElements();
610                 getLog().debug( "Using aux classpath: " + classpath );
611                 configuration.prependClasspath( StringUtils.join( classpath.iterator(), File.pathSeparator ) );
612             }
613             catch ( Exception e )
614             {
615                 throw new MavenReportException( e.getMessage(), e );
616             }
617         }
618 
619         if ( null != suppressMarker )
620         {
621             configuration.setSuppressMarker( suppressMarker );
622         }
623 
624         configuration.setBenchmark( benchmark );
625 
626         return configuration;
627     }
628 
629     /**
630      * {@inheritDoc}
631      */
632     public String getOutputName()
633     {
634         return "pmd";
635     }
636 
637     private static ResourceBundle getBundle( Locale locale )
638     {
639         return ResourceBundle.getBundle( "pmd-report", locale, PmdReport.class.getClassLoader() );
640     }
641 
642     /**
643      * Create and return the correct renderer for the output type.
644      *
645      * @return the renderer based on the configured output
646      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
647      */
648     public final Renderer createRenderer()
649         throws MavenReportException
650     {
651         Renderer result = null;
652         if ( "xml".equals( format ) )
653         {
654             result = new XMLRenderer( getOutputEncoding() );
655         }
656         else if ( "txt".equals( format ) )
657         {
658             result = new TextRenderer();
659         }
660         else if ( "csv".equals( format ) )
661         {
662             result = new CSVRenderer();
663         }
664         else if ( "html".equals( format ) )
665         {
666             result = new HTMLRenderer();
667         }
668         else if ( !"".equals( format ) && !"none".equals( format ) )
669         {
670             try
671             {
672                 result = (Renderer) Class.forName( format ).getConstructor( Properties.class ).
673                                 newInstance( new Properties() );
674             }
675             catch ( Exception e )
676             {
677                 throw new MavenReportException( "Can't find PMD custom format " + format + ": "
678                     + e.getClass().getName(), e );
679             }
680         }
681 
682         return result;
683     }
684 }