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