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.FileInputStream;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStreamWriter;
29  import java.io.Reader;
30  import java.io.UnsupportedEncodingException;
31  import java.io.Writer;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.ResourceBundle;
35  
36  import net.sourceforge.pmd.IRuleViolation;
37  import net.sourceforge.pmd.PMD;
38  import net.sourceforge.pmd.PMDException;
39  import net.sourceforge.pmd.Report;
40  import net.sourceforge.pmd.Rule;
41  import net.sourceforge.pmd.RuleContext;
42  import net.sourceforge.pmd.RuleSet;
43  import net.sourceforge.pmd.RuleSetFactory;
44  import net.sourceforge.pmd.SourceType;
45  import net.sourceforge.pmd.renderers.CSVRenderer;
46  import net.sourceforge.pmd.renderers.HTMLRenderer;
47  import net.sourceforge.pmd.renderers.Renderer;
48  import net.sourceforge.pmd.renderers.TextRenderer;
49  import net.sourceforge.pmd.renderers.XMLRenderer;
50  
51  import org.apache.maven.doxia.sink.Sink;
52  import org.apache.maven.reporting.MavenReportException;
53  import org.codehaus.plexus.resource.ResourceManager;
54  import org.codehaus.plexus.resource.loader.FileResourceCreationException;
55  import org.codehaus.plexus.resource.loader.FileResourceLoader;
56  import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
57  import org.codehaus.plexus.util.FileUtils;
58  import org.codehaus.plexus.util.IOUtil;
59  import org.codehaus.plexus.util.ReaderFactory;
60  import org.codehaus.plexus.util.StringUtils;
61  
62  /**
63   * Creates a PMD report.
64   *
65   * @author Brett Porter
66   * @version $Id: PmdReport.html 816696 2012-05-08 15:19:20Z 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<File, PmdFileInfo> 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 ( Map.Entry<File, PmdFileInfo> entry : files.entrySet() )
263         {
264             File file = entry.getKey();
265             PmdFileInfo fileInfo = entry.getValue();
266 
267             // TODO: lazily call beginFile in case there are no rules
268 
269             reportSink.beginFile( file, fileInfo );
270             ruleContext.setSourceCodeFilename( file.getAbsolutePath() );
271             for ( int idx = 0; idx < rulesets.length; idx++ )
272             {
273                 try
274                 {
275                     // PMD closes this Reader even though it did not open it so we have
276                     // to open a new one with every call to processFile().
277                     Reader reader;
278                     if ( StringUtils.isNotEmpty( getSourceEncoding() ) )
279                     {
280                         reader = ReaderFactory.newReader( file, getSourceEncoding() );
281                     }
282                     else
283                     {
284                         reader = ReaderFactory.newPlatformReader( file );
285                     }
286 
287                     try
288                     {
289                         pmd.processFile( reader, sets[idx], ruleContext );
290                     }
291                     finally
292                     {
293                         reader.close();
294                     }
295                 }
296                 catch ( UnsupportedEncodingException e1 )
297                 {
298                     throw new MavenReportException( "Encoding '" + getSourceEncoding() + "' is not supported.", e1 );
299                 }
300                 catch ( PMDException pe )
301                 {
302                     String msg = pe.getLocalizedMessage();
303                     Throwable r = pe.getCause();
304                     if ( r != null )
305                     {
306                         msg = msg + ": " + r.getLocalizedMessage();
307                     }
308                     getLog().warn( msg );
309                     reportSink.ruleViolationAdded( new ProcessingErrorRuleViolation( file, msg ) );
310                 }
311                 catch ( FileNotFoundException e2 )
312                 {
313                     getLog().warn( "Error opening source file: " + file );
314                     reportSink.ruleViolationAdded( new ProcessingErrorRuleViolation( file, e2.getLocalizedMessage() ) );
315                 }
316                 catch ( Exception e3 )
317                 {
318                     getLog().warn( "Failure executing PMD for: " + file, e3 );
319                     reportSink.ruleViolationAdded( new ProcessingErrorRuleViolation( file, e3.getLocalizedMessage() ) );
320                 }
321             }
322             reportSink.endFile( file );
323         }
324 
325         for ( int idx = 0; idx < rulesets.length; idx++ )
326         {
327             sets[idx].end( ruleContext );
328         }
329 
330         reportSink.endDocument();
331 
332         return report;
333     }
334 
335     /**
336      * Use the PMD renderers to render in any format aside from HTML.
337      *
338      * @param report
339      * @throws MavenReportException
340      */
341     private void writeNonHtml( Report report )
342         throws MavenReportException
343     {
344         Renderer r = createRenderer();
345 
346         if ( r == null )
347         {
348             return;
349         }
350 
351         Writer writer = null;
352 
353         try
354         {
355             targetDirectory.mkdirs();
356             File targetFile = new File( targetDirectory, "pmd." + format );
357             FileOutputStream tStream = new FileOutputStream( targetFile );
358             writer = new OutputStreamWriter( tStream, getOutputEncoding() );
359 
360             r.setWriter( writer );
361             r.start();
362             r.renderFileReport( report );
363             r.end();
364             writer.close();
365 
366             File siteDir = getReportOutputDirectory();
367             siteDir.mkdirs();
368             FileUtils.copyFile( targetFile, new File( siteDir, "pmd." + format ) );
369         }
370         catch ( IOException ioe )
371         {
372             throw new MavenReportException( ioe.getMessage(), ioe );
373         }
374         finally
375         {
376             IOUtil.close( writer );
377         }
378     }
379 
380     /**
381      * Convenience method to get the location of the specified file name.
382      *
383      * @param name the name of the file whose location is to be resolved
384      * @return a String that contains the absolute file name of the file
385      */
386     protected String getLocationTemp( String name )
387     {
388         String loc = name;
389         if ( loc.indexOf( '/' ) != -1 )
390         {
391             loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
392         }
393         if ( loc.indexOf( '\\' ) != -1 )
394         {
395             loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
396         }
397 
398         // MPMD-127 in the case that the rules are defined externally on a url
399         // we need to replace some special url characters that cannot be
400         // used in filenames on disk or produce ackward filenames.
401         // replace all occurrences of the following characters:  ? : & = %
402         loc = loc.replaceAll( "[\\?\\:\\&\\=\\%]", "_" );
403 
404         getLog().debug( "Before: " + name + " After: " + loc );
405         return loc;
406     }
407 
408     /**
409      * Constructs the PMD class, passing it an argument
410      * that configures the target JDK.
411      *
412      * @return the resulting PMD
413      * @throws org.apache.maven.reporting.MavenReportException
414      *          if targetJdk is not supported
415      */
416     public PMD getPMD()
417         throws MavenReportException
418     {
419         PMD pmd = new PMD();
420 
421         if ( null != targetJdk )
422         {
423             SourceType sourceType = SourceType.getSourceTypeForId( "java " + targetJdk );
424             if ( sourceType == null )
425             {
426                 throw new MavenReportException( "Unsupported targetJdk value '" + targetJdk + "'." );
427             }
428             pmd.setJavaVersion( sourceType );
429         }
430 
431         return pmd;
432     }
433 
434     /**
435      * {@inheritDoc}
436      */
437     public String getOutputName()
438     {
439         return "pmd";
440     }
441 
442     private static ResourceBundle getBundle( Locale locale )
443     {
444         return ResourceBundle.getBundle( "pmd-report", locale, PmdReport.class.getClassLoader() );
445     }
446 
447     /**
448      * Create and return the correct renderer for the output type.
449      *
450      * @return the renderer based on the configured output
451      * @throws org.apache.maven.reporting.MavenReportException
452      *          if no renderer found for the output type
453      */
454     public final Renderer createRenderer()
455         throws MavenReportException
456     {
457         Renderer renderer = null;
458         if ( "xml".equals( format ) )
459         {
460             renderer = new PmdXMLRenderer( getOutputEncoding() );
461         }
462         else if ( "txt".equals( format ) )
463         {
464             renderer = new TextRenderer();
465         }
466         else if ( "csv".equals( format ) )
467         {
468             renderer = new CSVRenderer();
469         }
470         else if ( "html".equals( format ) )
471         {
472             renderer = new HTMLRenderer();
473         }
474         else if ( !"".equals( format ) && !"none".equals( format ) )
475         {
476             try
477             {
478                 renderer = (Renderer) Class.forName( format ).newInstance();
479             }
480             catch ( Exception e )
481             {
482                 throw new MavenReportException(
483                     "Can't find PMD custom format " + format + ": " + e.getClass().getName(), e );
484             }
485         }
486 
487         return renderer;
488     }
489 
490     private static class PmdXMLRenderer
491         extends XMLRenderer
492     {
493         public PmdXMLRenderer( String encoding )
494         {
495             super();
496             this.encoding = encoding;
497         }
498     }
499 
500     /**
501      * @author <a href="mailto:douglass.doug@gmail.com">Doug Douglass</a>
502      */
503     private static class ProcessingErrorRuleViolation
504         implements IRuleViolation
505     {
506 
507         private String filename;
508 
509         private String description;
510 
511         public ProcessingErrorRuleViolation( File file, String description )
512         {
513             filename = file.getPath();
514             this.description = description;
515         }
516 
517         /**
518          * {@inheritDoc}
519          */
520         public String getFilename()
521         {
522             return this.filename;
523         }
524 
525         /**
526          * {@inheritDoc}
527          */
528         public int getBeginLine()
529         {
530             return 0;
531         }
532 
533         /**
534          * {@inheritDoc}
535          */
536         public int getBeginColumn()
537         {
538             return 0;
539         }
540 
541         /**
542          * {@inheritDoc}
543          */
544         public int getEndLine()
545         {
546             return 0;
547         }
548 
549         /**
550          * {@inheritDoc}
551          */
552         public int getEndColumn()
553         {
554             return 0;
555         }
556 
557         /**
558          * {@inheritDoc}
559          */
560         public Rule getRule()
561         {
562             return null;
563         }
564 
565         /**
566          * {@inheritDoc}
567          */
568         public String getDescription()
569         {
570             return this.description;
571         }
572 
573         /**
574          * {@inheritDoc}
575          */
576         public String getPackageName()
577         {
578             return null;
579         }
580 
581         /**
582          * {@inheritDoc}
583          */
584         public String getMethodName()
585         {
586             return null;
587         }
588 
589         /**
590          * {@inheritDoc}
591          */
592         public String getClassName()
593         {
594             return null;
595         }
596 
597         /**
598          * {@inheritDoc}
599          */
600         public boolean isSuppressed()
601         {
602             return false;
603         }
604 
605         /**
606          * {@inheritDoc}
607          */
608         public String getVariableName()
609         {
610             return null;
611         }
612     }
613 }