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