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.RuleViolation;
30  import net.sourceforge.pmd.lang.LanguageVersion;
31  import net.sourceforge.pmd.renderers.CSVRenderer;
32  import net.sourceforge.pmd.renderers.HTMLRenderer;
33  import net.sourceforge.pmd.renderers.Renderer;
34  import net.sourceforge.pmd.renderers.TextRenderer;
35  import net.sourceforge.pmd.renderers.XMLRenderer;
36  import net.sourceforge.pmd.util.datasource.DataSource;
37  import net.sourceforge.pmd.util.datasource.FileDataSource;
38  import org.apache.maven.doxia.sink.Sink;
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.IOUtil;
50  import org.codehaus.plexus.util.ReaderFactory;
51  import org.codehaus.plexus.util.StringUtils;
52  
53  import java.io.File;
54  import java.io.FileOutputStream;
55  import java.io.IOException;
56  import java.io.OutputStreamWriter;
57  import java.io.Writer;
58  import java.util.ArrayList;
59  import java.util.Collections;
60  import java.util.List;
61  import java.util.Locale;
62  import java.util.Map;
63  import java.util.Properties;
64  import java.util.ResourceBundle;
65  
66  /**
67   * Creates a PMD report.
68   *
69   * @author Brett Porter
70   * @version $Id: PmdReport.html 853015 2013-03-04 21:10:54Z olamy $
71   * @since 2.0
72   */
73  @Mojo( name = "pmd", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST )
74  public class PmdReport
75      extends AbstractPmdReport
76  {
77      /**
78       * The target JDK to analyze based on. Should match the target used in the compiler plugin. Valid values are
79       * currently <code>1.3</code>, <code>1.4</code>, <code>1.5</code>, <code>1.6</code> and <code>1.7</code>.
80       * <p/>
81       * <b>Note:</b> support for <code>1.6</code> was added in version 2.3 of this plugin,
82       * support for <code>1.7</code> was added in version 2.7 of this plugin.
83       */
84      @Parameter( property = "targetJdk" )
85      private String targetJdk;
86  
87      /**
88       * The programming language to be analyzed by PMD. Valid values are currently <code>java</code>
89       * and <code>ecmascript</code> or <code>javascript</code>.
90       * <p>
91       * <b>Note:</b> if the parameter targetJdk is given, then this language parameter will be ignored.
92       * </p>
93       *
94       * @since 3.0
95       */
96      @Parameter( defaultValue = "java" )
97      private String language;
98  
99      /**
100      * The rule priority threshold; rules with lower priority
101      * than this will not be evaluated.
102      *
103      * @since 2.1
104      */
105     @Parameter( property = "minimumPriority", defaultValue = "5" )
106     private int minimumPriority = 5;
107 
108     /**
109      * Skip the PMD report generation.  Most useful on the command line
110      * 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. Since version 2.5, the ruleset "rulesets/maven.xml" is also available. Defaults to the
120      * java-basic, java-imports and java-unusedcode rulesets.
121      */
122     @Parameter
123     private String[] rulesets = new String[]{ "java-basic", "java-unusedcode", "java-imports" };
124 
125     /**
126      * Controls whether the project's compile/test classpath should be passed to PMD to enable its type resolution
127      * feature.
128      * 
129      * @since 3.0
130      */
131     @Parameter( property = "pmd.typeResolution", defaultValue = "false" )
132     private boolean typeResolution;
133 
134     /**
135      */
136     @Component
137     private ResourceManager locator;
138 
139     /**
140      * {@inheritDoc}
141      */
142     public String getName( Locale locale )
143     {
144         return getBundle( locale ).getString( "report.pmd.name" );
145     }
146 
147     /**
148      * {@inheritDoc}
149      */
150     public String getDescription( Locale locale )
151     {
152         return getBundle( locale ).getString( "report.pmd.description" );
153     }
154 
155     public void setRulesets( String[] rules )
156     {
157         rulesets = rules;
158     }
159 
160     /**
161      * {@inheritDoc}
162      */
163     public void executeReport( Locale locale )
164         throws MavenReportException
165     {
166         try
167         {
168             execute( locale );
169         }
170         finally
171         {
172             if ( getSink() != null )
173             {
174                 getSink().close();
175             }
176         }
177     }
178 
179     private void execute( Locale locale )
180         throws MavenReportException
181     {
182         //configure ResourceManager
183         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
184         locator.addSearchPath( "url", "" );
185         locator.setOutputDirectory( targetDirectory );
186 
187         if ( !skip && canGenerateReport() )
188         {
189             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
190             try
191             {
192                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
193 
194                 Report report = generateReport( locale );
195 
196                 if ( !isHtml() )
197                 {
198                     writeNonHtml( report );
199                 }
200             }
201             finally
202             {
203                 Thread.currentThread().setContextClassLoader( origLoader );
204             }
205         }
206     }
207 
208     private Report generateReport( Locale locale )
209         throws MavenReportException
210     {
211         Sink sink = getSink();
212 
213         PMDConfiguration pmdConfiguration = getPMDConfiguration();
214         final PmdReportListener reportSink = new PmdReportListener( getLog(), sink, getBundle( locale ), aggregate );
215         RuleContext ruleContext = new RuleContext()
216         {
217             @Override
218             public void setReport( Report report )
219             {
220                 super.setReport( report );
221                 // make sure our listener is added - the Report is created by PMD internally now
222                 report.addListener( reportSink );
223             }
224         };
225         reportSink.beginDocument();
226 
227         RuleSetFactory ruleSetFactory = new RuleSetFactory();
228         ruleSetFactory.setMinimumPriority( RulePriority.valueOf( this.minimumPriority ) );
229         String[] sets = new String[rulesets.length];
230         try
231         {
232             for ( int idx = 0; idx < rulesets.length; idx++ )
233             {
234                 String set = rulesets[idx];
235                 getLog().debug( "Preparing ruleset: " + set );
236                 RuleSetReferenceId id = new RuleSetReferenceId( set );
237                 File ruleset = locator.getResourceAsFile( id.getRuleSetFileName(), getLocationTemp( set ) );
238                 if ( null == ruleset )
239                 {
240                     throw new MavenReportException( "Could not resolve " + set );
241                 }
242                 sets[idx] = ruleset.getAbsolutePath();
243             }
244         }
245         catch ( ResourceNotFoundException e )
246         {
247             throw new MavenReportException( e.getMessage(), e );
248         }
249         catch ( FileResourceCreationException e )
250         {
251             throw new MavenReportException( e.getMessage(), e );
252         }
253         pmdConfiguration.setRuleSets( StringUtils.join( sets, "," ) );
254 
255         Map<File, PmdFileInfo> files;
256         try
257         {
258             files = getFilesToProcess();
259             if ( files.isEmpty() && !"java".equals( language ) )
260             {
261                 getLog().warn(
262                     "No files found to process. Did you add your additional source folders like javascript? (see also build-helper-maven-plugin)" );
263             }
264         }
265         catch ( IOException e )
266         {
267             throw new MavenReportException( "Can't get file list", e );
268         }
269 
270         String encoding = getSourceEncoding();
271         if ( StringUtils.isEmpty( encoding ) && !files.isEmpty() )
272         {
273             getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
274                                + ", i.e. build is platform dependent!" );
275             encoding = ReaderFactory.FILE_ENCODING;
276         }
277         pmdConfiguration.setSourceEncoding( encoding );
278 
279         reportSink.setFiles( files );
280         List<DataSource> dataSources = new ArrayList<DataSource>( files.size() );
281         for ( File f : files.keySet() )
282         {
283             dataSources.add( new FileDataSource( f ) );
284         }
285 
286         try
287         {
288             List<Renderer> renderers = Collections.emptyList();
289 
290             // Unfortunately we need to disable multi-threading for now - as otherwise our PmdReportListener
291             // will be ignored.
292             // Longer term solution could be to use a custom renderer instead. And collect with this renderer
293             // all the violations.
294             pmdConfiguration.setThreads( 0 );
295 
296             PMD.processFiles( pmdConfiguration, ruleSetFactory, dataSources, ruleContext, renderers );
297         }
298         catch ( Exception e )
299         {
300             getLog().warn( "Failure executing PMD: " + e.getLocalizedMessage(), e );
301         }
302 
303         try
304         {
305             reportSink.endDocument();
306         }
307         catch ( IOException e )
308         {
309             getLog().warn( "Failure creating the report: " + e.getLocalizedMessage(), e );
310         }
311 
312         // copy over the violations into a single report - PMD now creates one report per file
313         Report report = new Report();
314         for ( RuleViolation v : reportSink.getViolations() )
315         {
316             report.addRuleViolation( v );
317         }
318         return report;
319     }
320 
321     /**
322      * Convenience method to get the location of the specified file name.
323      *
324      * @param name the name of the file whose location is to be resolved
325      * @return a String that contains the absolute file name of the file
326      */
327     protected String getLocationTemp( String name )
328     {
329         String loc = name;
330         if ( loc.indexOf( '/' ) != -1 )
331         {
332             loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
333         }
334         if ( loc.indexOf( '\\' ) != -1 )
335         {
336             loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
337         }
338 
339         // MPMD-127 in the case that the rules are defined externally on a url
340         // we need to replace some special url characters that cannot be
341         // used in filenames on disk or produce ackward filenames.
342         // replace all occurrences of the following characters:  ? : & = %
343         loc = loc.replaceAll( "[\\?\\:\\&\\=\\%]", "_" );
344 
345         if ( !loc.endsWith( ".xml" ) )
346         {
347             loc = loc + ".xml";
348         }
349 
350         getLog().debug( "Before: " + name + " After: " + loc );
351         return loc;
352     }
353 
354     /**
355      * Use the PMD renderers to render in any format aside from HTML.
356      *
357      * @param report
358      * @throws MavenReportException
359      */
360     private void writeNonHtml( Report report )
361         throws MavenReportException
362     {
363         Renderer r = createRenderer();
364 
365         if ( r == null )
366         {
367             return;
368         }
369 
370         Writer writer = null;
371         FileOutputStream tStream = null;
372         try
373         {
374             targetDirectory.mkdirs();
375             File targetFile = new File( targetDirectory, "pmd." + format );
376             tStream = new FileOutputStream( targetFile );
377             writer = new OutputStreamWriter( tStream, getOutputEncoding() );
378 
379             r.setWriter( writer );
380             r.start();
381             r.renderFileReport( report );
382             r.end();
383             writer.close();
384 
385             if ( includeXmlInSite ) {
386                 File siteDir = getReportOutputDirectory();
387                 siteDir.mkdirs();
388                 FileUtils.copyFile( targetFile, new File( siteDir, "pmd." + format ) );
389             }
390         }
391         catch ( IOException ioe )
392         {
393             throw new MavenReportException( ioe.getMessage(), ioe );
394         }
395         finally
396         {
397             IOUtil.close( writer );
398             IOUtil.close( tStream );
399         }
400     }
401 
402     /**
403      * Constructs the PMD configuration class, passing it an argument
404      * that configures the target JDK.
405      *
406      * @return the resulting PMD
407      * @throws org.apache.maven.reporting.MavenReportException
408      *          if targetJdk is not supported
409      */
410     public PMDConfiguration getPMDConfiguration()
411         throws MavenReportException
412     {
413         PMDConfiguration configuration = new PMDConfiguration();
414         LanguageVersion languageVersion = null;
415 
416         if ( null != targetJdk )
417         {
418             languageVersion = LanguageVersion.findByTerseName( "java " + targetJdk );
419             if ( languageVersion == null )
420             {
421                 throw new MavenReportException( "Unsupported targetJdk value '" + targetJdk + "'." );
422             }
423         }
424         else if ( "javascript".equals( language ) || "ecmascript".equals( language ) )
425         {
426             languageVersion = LanguageVersion.ECMASCRIPT;
427         }
428         if ( languageVersion != null )
429         {
430             getLog().debug( "Using language " + languageVersion );
431             configuration.setDefaultLanguageVersion( languageVersion );
432         }
433 
434         if ( typeResolution )
435         {
436             try
437             {
438                 @SuppressWarnings( "unchecked" )
439                 List<String> classpath =
440                     includeTests ? project.getTestClasspathElements() : project.getCompileClasspathElements();
441                 getLog().debug( "Using aux classpath: " + classpath );
442                 configuration.prependClasspath( StringUtils.join( classpath.iterator(), File.pathSeparator ) );
443             }
444             catch ( Exception e )
445             {
446                 throw new MavenReportException( e.getMessage(), e );
447             }
448         }
449 
450         return configuration;
451     }
452 
453     /**
454      * {@inheritDoc}
455      */
456     public String getOutputName()
457     {
458         return "pmd";
459     }
460 
461     private static ResourceBundle getBundle( Locale locale )
462     {
463         return ResourceBundle.getBundle( "pmd-report", locale, PmdReport.class.getClassLoader() );
464     }
465 
466     /**
467      * Create and return the correct renderer for the output type.
468      *
469      * @return the renderer based on the configured output
470      * @throws org.apache.maven.reporting.MavenReportException
471      *          if no renderer found for the output type
472      */
473     public final Renderer createRenderer()
474         throws MavenReportException
475     {
476         Renderer renderer = null;
477         if ( "xml".equals( format ) )
478         {
479             renderer = new XMLRenderer( getOutputEncoding() );
480         }
481         else if ( "txt".equals( format ) )
482         {
483             renderer = new TextRenderer( );
484         }
485         else if ( "csv".equals( format ) )
486         {
487             renderer = new CSVRenderer( );
488         }
489         else if ( "html".equals( format ) )
490         {
491             renderer = new HTMLRenderer( );
492         }
493         else if ( !"".equals( format ) && !"none".equals( format ) )
494         {
495             try
496             {
497                 renderer = (Renderer) Class.forName( format ).getConstructor( Properties.class ).newInstance(
498                     new Properties() );
499             }
500             catch ( Exception e )
501             {
502                 throw new MavenReportException(
503                     "Can't find PMD custom format " + format + ": " + e.getClass().getName(), e );
504             }
505         }
506 
507         return renderer;
508     }
509 
510 }