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.IRuleViolation;
23  import net.sourceforge.pmd.PMD;
24  import net.sourceforge.pmd.PMDException;
25  import net.sourceforge.pmd.Report;
26  import net.sourceforge.pmd.Rule;
27  import net.sourceforge.pmd.RuleContext;
28  import net.sourceforge.pmd.RuleSet;
29  import net.sourceforge.pmd.RuleSetFactory;
30  import net.sourceforge.pmd.SourceType;
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 org.apache.maven.doxia.sink.Sink;
37  import org.apache.maven.reporting.MavenReportException;
38  import org.codehaus.plexus.resource.ResourceManager;
39  import org.codehaus.plexus.resource.loader.FileResourceCreationException;
40  import org.codehaus.plexus.resource.loader.FileResourceLoader;
41  import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
42  import org.codehaus.plexus.util.FileUtils;
43  import org.codehaus.plexus.util.IOUtil;
44  import org.codehaus.plexus.util.ReaderFactory;
45  import org.codehaus.plexus.util.StringUtils;
46  
47  import java.io.File;
48  import java.io.FileInputStream;
49  import java.io.FileNotFoundException;
50  import java.io.FileOutputStream;
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.io.OutputStreamWriter;
54  import java.io.Reader;
55  import java.io.UnsupportedEncodingException;
56  import java.io.Writer;
57  import java.util.Iterator;
58  import java.util.Locale;
59  import java.util.Map;
60  import java.util.ResourceBundle;
61  
62  /**
63   * Creates a PMD report.
64   *
65   * @author Brett Porter
66   * @version $Id: PmdReport.html 816691 2012-05-08 15:16:42Z hboutemy $
67   * @goal pmd
68   * @threadSafe
69   * @since 2.0
70   */
71  public class PmdReport
72      extends AbstractPmdReport
73  {
74      /**
75       * The target JDK to analyze based on. Should match the target used in the compiler plugin. Valid values are
76       * currently <code>1.3</code>, <code>1.4</code>, <code>1.5</code> and <code>1.6</code>.
77       * <p>
78       * <b>Note:</b> support for <code>1.6</code> was added in version 2.3 of this plugin.
79       * </p>
80       *
81       * @parameter expression="${targetJdk}"
82       */
83      private String targetJdk;
84  
85      /**
86       * The rule priority threshold; rules with lower priority
87       * than this will not be evaluated.
88       *
89       * @parameter expression="${minimumPriority}" default-value="5"
90       * @since 2.1
91       */
92      private int minimumPriority = 5;
93  
94      /**
95       * Skip the PMD report generation.  Most useful on the command line
96       * via "-Dpmd.skip=true".
97       *
98       * @parameter expression="${pmd.skip}" default-value="false"
99       * @since 2.1
100      */
101     private boolean skip;
102 
103     /**
104      * The PMD rulesets to use. See the <a href="http://pmd.sourceforge.net/rules/index.html">Stock Rulesets</a> for a
105      * list of some included. Since version 2.5, the ruleset "rulesets/maven.xml" is also available. Defaults to the
106      * basic, imports and unusedcode rulesets.
107      *
108      * @parameter
109      */
110     private String[] rulesets =
111         new String[]{ "rulesets/basic.xml", "rulesets/unusedcode.xml", "rulesets/imports.xml", };
112 
113     /**
114      * @component
115      * @required
116      * @readonly
117      */
118     private ResourceManager locator;
119 
120     /**
121      * {@inheritDoc}
122      */
123     public String getName( Locale locale )
124     {
125         return getBundle( locale ).getString( "report.pmd.name" );
126     }
127 
128     /**
129      * {@inheritDoc}
130      */
131     public String getDescription( Locale locale )
132     {
133         return getBundle( locale ).getString( "report.pmd.description" );
134     }
135 
136     public void setRulesets( String[] rules )
137     {
138         rulesets = rules;
139     }
140 
141     /**
142      * {@inheritDoc}
143      */
144     public void executeReport( Locale locale )
145         throws MavenReportException
146     {
147         try
148         {
149             execute( locale );
150         }
151         finally
152         {
153             if ( getSink() != null )
154             {
155                 getSink().close();
156             }
157         }
158     }
159 
160     private void execute( Locale locale )
161         throws MavenReportException
162     {
163         //configure ResourceManager
164         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
165         locator.addSearchPath( "url", "" );
166         locator.setOutputDirectory( targetDirectory );
167 
168         if ( !skip && canGenerateReport() )
169         {
170             ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
171             try
172             {
173                 Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() );
174 
175                 Report report = generateReport( locale );
176 
177                 if ( !isHtml() )
178                 {
179                     writeNonHtml( report );
180                 }
181             }
182             finally
183             {
184                 Thread.currentThread().setContextClassLoader( origLoader );
185             }
186         }
187     }
188 
189     private Report generateReport( Locale locale )
190         throws MavenReportException
191     {
192         Sink sink = getSink();
193 
194         PMD pmd = getPMD();
195         RuleContext ruleContext = new RuleContext();
196         Report report = new Report();
197         PmdReportListener reportSink = new PmdReportListener( sink, getBundle( locale ), aggregate );
198 
199         report.addListener( reportSink );
200         ruleContext.setReport( report );
201         reportSink.beginDocument();
202 
203         RuleSetFactory ruleSetFactory = new RuleSetFactory();
204         ruleSetFactory.setMinimumPriority( this.minimumPriority );
205         RuleSet[] sets = new RuleSet[rulesets.length];
206         try
207         {
208             for ( int idx = 0; idx < rulesets.length; idx++ )
209             {
210                 String set = rulesets[idx];
211                 getLog().debug( "Preparing ruleset: " + set );
212                 File ruleset = locator.getResourceAsFile( set, getLocationTemp( set ) );
213 
214                 if ( null == ruleset )
215                 {
216                     throw new MavenReportException( "Could not resolve " + set );
217                 }
218 
219                 InputStream rulesInput = new FileInputStream( ruleset );
220                 try
221                 {
222                     RuleSet ruleSet = ruleSetFactory.createRuleSet( rulesInput );
223                     sets[idx] = ruleSet;
224 
225                     ruleSet.start( ruleContext );
226                 }
227                 finally
228                 {
229                     rulesInput.close();
230                 }
231             }
232         }
233         catch ( IOException e )
234         {
235             throw new MavenReportException( e.getMessage(), e );
236         }
237         catch ( ResourceNotFoundException e )
238         {
239             throw new MavenReportException( e.getMessage(), e );
240         }
241         catch ( FileResourceCreationException e )
242         {
243             throw new MavenReportException( e.getMessage(), e );
244         }
245 
246         Map files;
247         try
248         {
249             files = getFilesToProcess();
250         }
251         catch ( IOException e )
252         {
253             throw new MavenReportException( "Can't get file list", e );
254         }
255 
256         if ( StringUtils.isEmpty( getSourceEncoding() ) && !files.isEmpty() )
257         {
258             getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
259                                + ", i.e. build is platform dependent!" );
260         }
261 
262         for ( Iterator i = files.entrySet().iterator(); i.hasNext(); )
263         {
264             Map.Entry entry = (Map.Entry) i.next();
265             File file = (File) entry.getKey();
266             PmdFileInfo fileInfo = (PmdFileInfo) entry.getValue();
267 
268             // TODO: lazily call beginFile in case there are no rules
269 
270             reportSink.beginFile( file, fileInfo );
271             ruleContext.setSourceCodeFilename( file.getAbsolutePath() );
272             for ( int idx = 0; idx < rulesets.length; idx++ )
273             {
274                 try
275                 {
276                     // PMD closes this Reader even though it did not open it so we have
277                     // to open a new one with every call to processFile().
278                     Reader reader;
279                     if ( StringUtils.isNotEmpty( getSourceEncoding() ) )
280                     {
281                         reader = ReaderFactory.newReader( file, getSourceEncoding() );
282                     }
283                     else
284                     {
285                         reader = ReaderFactory.newPlatformReader( file );
286                     }
287 
288                     try
289                     {
290                         pmd.processFile( reader, sets[idx], ruleContext );
291                     }
292                     finally
293                     {
294                         reader.close();
295                     }
296                 }
297                 catch ( UnsupportedEncodingException e1 )
298                 {
299                     throw new MavenReportException( "Encoding '" + getSourceEncoding() + "' is not supported.", e1 );
300                 }
301                 catch ( PMDException pe )
302                 {
303                     String msg = pe.getLocalizedMessage();
304                     Throwable r = pe.getCause();
305                     if ( r != null )
306                     {
307                         msg = msg + ": " + r.getLocalizedMessage();
308                     }
309                     getLog().warn( msg );
310                     reportSink.ruleViolationAdded( new ProcessingErrorRuleViolation( file, msg ) );
311                 }
312                 catch ( FileNotFoundException e2 )
313                 {
314                     getLog().warn( "Error opening source file: " + file );
315                     reportSink.ruleViolationAdded( new ProcessingErrorRuleViolation( file, e2.getLocalizedMessage() ) );
316                 }
317                 catch ( Exception e3 )
318                 {
319                     getLog().warn( "Failure executing PMD for: " + file, e3 );
320                     reportSink.ruleViolationAdded( new ProcessingErrorRuleViolation( file, e3.getLocalizedMessage() ) );
321                 }
322             }
323             reportSink.endFile( file );
324         }
325 
326         for ( int idx = 0; idx < rulesets.length; idx++ )
327         {
328             sets[idx].end( ruleContext );
329         }
330 
331         reportSink.endDocument();
332 
333         return report;
334     }
335 
336     /**
337      * Use the PMD renderers to render in any format aside from HTML.
338      *
339      * @param report
340      * @throws MavenReportException
341      */
342     private void writeNonHtml( Report report )
343         throws MavenReportException
344     {
345         Renderer r = createRenderer();
346 
347         if ( r == null )
348         {
349             return;
350         }
351 
352         Writer writer = null;
353 
354         try
355         {
356             targetDirectory.mkdirs();
357             File targetFile = new File( targetDirectory, "pmd." + format );
358             FileOutputStream tStream = new FileOutputStream( targetFile );
359             writer = new OutputStreamWriter( tStream, getOutputEncoding() );
360 
361             r.setWriter( writer );
362             r.start();
363             r.renderFileReport( report );
364             r.end();
365             writer.close();
366 
367             File siteDir = getReportOutputDirectory();
368             siteDir.mkdirs();
369             FileUtils.copyFile( targetFile, new File( siteDir, "pmd." + format ) );
370         }
371         catch ( IOException ioe )
372         {
373             throw new MavenReportException( ioe.getMessage(), ioe );
374         }
375         finally
376         {
377             IOUtil.close( writer );
378         }
379     }
380 
381     /**
382      * Convenience method to get the location of the specified file name.
383      *
384      * @param name the name of the file whose location is to be resolved
385      * @return a String that contains the absolute file name of the file
386      */
387     protected String getLocationTemp( String name )
388     {
389         String loc = name;
390         if ( loc.indexOf( '/' ) != -1 )
391         {
392             loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
393         }
394         if ( loc.indexOf( '\\' ) != -1 )
395         {
396             loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
397         }
398 
399         // MPMD-127 in the case that the rules are defined externally on a url
400         // we need to replace some special url characters that cannot be
401         // used in filenames on disk or produce ackward filenames.
402         // replace all occurrences of the following characters:  ? : & = %
403         loc = loc.replaceAll( "[\\?\\:\\&\\=\\%]", "_" );
404 
405         getLog().debug( "Before: " + name + " After: " + loc );
406         return loc;
407     }
408 
409     /**
410      * Constructs the PMD class, passing it an argument
411      * that configures the target JDK.
412      *
413      * @return the resulting PMD
414      * @throws org.apache.maven.reporting.MavenReportException
415      *          if targetJdk is not supported
416      */
417     public PMD getPMD()
418         throws MavenReportException
419     {
420         PMD pmd = new PMD();
421 
422         if ( null != targetJdk )
423         {
424             SourceType sourceType = SourceType.getSourceTypeForId( "java " + targetJdk );
425             if ( sourceType == null )
426             {
427                 throw new MavenReportException( "Unsupported targetJdk value '" + targetJdk + "'." );
428             }
429             pmd.setJavaVersion( sourceType );
430         }
431 
432         return pmd;
433     }
434 
435     /**
436      * {@inheritDoc}
437      */
438     public String getOutputName()
439     {
440         return "pmd";
441     }
442 
443     private static ResourceBundle getBundle( Locale locale )
444     {
445         return ResourceBundle.getBundle( "pmd-report", locale, PmdReport.class.getClassLoader() );
446     }
447 
448     /**
449      * Create and return the correct renderer for the output type.
450      *
451      * @return the renderer based on the configured output
452      * @throws org.apache.maven.reporting.MavenReportException
453      *          if no renderer found for the output type
454      */
455     public final Renderer createRenderer()
456         throws MavenReportException
457     {
458         Renderer renderer = null;
459         if ( "xml".equals( format ) )
460         {
461             renderer = new PmdXMLRenderer( getOutputEncoding() );
462         }
463         else if ( "txt".equals( format ) )
464         {
465             renderer = new TextRenderer();
466         }
467         else if ( "csv".equals( format ) )
468         {
469             renderer = new CSVRenderer();
470         }
471         else if ( "html".equals( format ) )
472         {
473             renderer = new HTMLRenderer();
474         }
475         else if ( !"".equals( format ) && !"none".equals( format ) )
476         {
477             try
478             {
479                 renderer = (Renderer) Class.forName( format ).newInstance();
480             }
481             catch ( Exception e )
482             {
483                 throw new MavenReportException(
484                     "Can't find PMD custom format " + format + ": " + e.getClass().getName(), e );
485             }
486         }
487 
488         return renderer;
489     }
490 
491     private static class PmdXMLRenderer
492         extends XMLRenderer
493     {
494         public PmdXMLRenderer( String encoding )
495         {
496             super();
497             this.encoding = encoding;
498         }
499     }
500 
501     /**
502      * @author <a href="mailto:douglass.doug@gmail.com">Doug Douglass</a>
503      */
504     private static class ProcessingErrorRuleViolation
505         implements IRuleViolation
506     {
507 
508         private String filename;
509 
510         private String description;
511 
512         public ProcessingErrorRuleViolation( File file, String description )
513         {
514             filename = file.getPath();
515             this.description = description;
516         }
517 
518         /**
519          * {@inheritDoc}
520          */
521         public String getFilename()
522         {
523             return this.filename;
524         }
525 
526         /**
527          * {@inheritDoc}
528          */
529         public int getBeginLine()
530         {
531             return 0;
532         }
533 
534         /**
535          * {@inheritDoc}
536          */
537         public int getBeginColumn()
538         {
539             return 0;
540         }
541 
542         /**
543          * {@inheritDoc}
544          */
545         public int getEndLine()
546         {
547             return 0;
548         }
549 
550         /**
551          * {@inheritDoc}
552          */
553         public int getEndColumn()
554         {
555             return 0;
556         }
557 
558         /**
559          * {@inheritDoc}
560          */
561         public Rule getRule()
562         {
563             return null;
564         }
565 
566         /**
567          * {@inheritDoc}
568          */
569         public String getDescription()
570         {
571             return this.description;
572         }
573 
574         /**
575          * {@inheritDoc}
576          */
577         public String getPackageName()
578         {
579             return null;
580         }
581 
582         /**
583          * {@inheritDoc}
584          */
585         public String getMethodName()
586         {
587             return null;
588         }
589 
590         /**
591          * {@inheritDoc}
592          */
593         public String getClassName()
594         {
595             return null;
596         }
597 
598         /**
599          * {@inheritDoc}
600          */
601         public boolean isSuppressed()
602         {
603             return false;
604         }
605 
606         /**
607          * {@inheritDoc}
608          */
609         public String getVariableName()
610         {
611             return null;
612         }
613     }
614 }