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