View Javadoc
1   package org.apache.maven.plugins.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.IOException;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.Comparator;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.ResourceBundle;
32  import java.util.Set;
33  
34  import org.apache.maven.doxia.sink.Sink;
35  import org.apache.maven.plugin.logging.Log;
36  import org.codehaus.plexus.util.StringUtils;
37  
38  import net.sourceforge.pmd.Report.ProcessingError;
39  import net.sourceforge.pmd.RuleViolation;
40  
41  /**
42   * Render the PMD violations into Doxia events.
43   *
44   * @author Brett Porter
45   * @version $Id$
46   */
47  public class PmdReportGenerator
48  {
49      private Log log;
50  
51      private Sink sink;
52  
53      private String currentFilename;
54  
55      private ResourceBundle bundle;
56  
57      private Set<RuleViolation> violations = new HashSet<>();
58  
59      private List<ProcessingError> processingErrors = new ArrayList<>();
60  
61      private boolean aggregate;
62  
63      private boolean renderRuleViolationPriority;
64  
65      // The number of erroneous files
66      private int fileCount = 0;
67  
68      private Map<File, PmdFileInfo> files;
69  
70      // private List<Metric> metrics = new ArrayList<Metric>();
71  
72      public PmdReportGenerator( Log log, Sink sink, ResourceBundle bundle, boolean aggregate )
73      {
74          this.log = log;
75          this.sink = sink;
76          this.bundle = bundle;
77          this.aggregate = aggregate;
78      }
79  
80      private String getTitle()
81      {
82          return bundle.getString( "report.pmd.title" );
83      }
84  
85      public void setViolations( Collection<RuleViolation> violations )
86      {
87          this.violations = new HashSet<>( violations );
88      }
89  
90      public List<RuleViolation> getViolations()
91      {
92          return new ArrayList<>( violations );
93      }
94  
95      public void setProcessingErrors( Collection<ProcessingError> errors )
96      {
97          this.processingErrors = new ArrayList<>( errors );
98      }
99  
100     public List<ProcessingError> getProcessingErrors()
101     {
102         return processingErrors;
103     }
104 
105     // public List<Metric> getMetrics()
106     // {
107     // return metrics;
108     // }
109     //
110     // public void setMetrics( List<Metric> metrics )
111     // {
112     // this.metrics = metrics;
113     // }
114 
115     private String shortenFilename( String filename, PmdFileInfo fileInfo )
116     {
117         String result = filename;
118         if ( fileInfo != null && fileInfo.getSourceDirectory() != null )
119         {
120             result = StringUtils.substring( result, fileInfo.getSourceDirectory().getAbsolutePath().length() + 1 );
121         }
122         return StringUtils.replace( result, "\\", "/" );
123     }
124 
125     private String makeFileSectionName( String filename, PmdFileInfo fileInfo )
126     {
127         if ( aggregate && fileInfo != null && fileInfo.getProject() != null )
128         {
129             return fileInfo.getProject().getName() + " - " + filename;
130         }
131         return filename;
132     }
133 
134     private PmdFileInfo determineFileInfo( String filename )
135         throws IOException
136     {
137         File canonicalFilename = new File( filename ).getCanonicalFile();
138         PmdFileInfo fileInfo = files.get( canonicalFilename );
139         if ( fileInfo == null )
140         {
141             log.warn( "Couldn't determine PmdFileInfo for file " + filename + " (canonical: " + canonicalFilename
142                 + "). XRef links won't be available." );
143         }
144 
145         return fileInfo;
146     }
147 
148     private void startFileSection( String currentFilename, PmdFileInfo fileInfo )
149     {
150         sink.section2();
151         sink.sectionTitle2();
152 
153         // prepare the filename
154         this.currentFilename = shortenFilename( currentFilename, fileInfo );
155 
156         sink.text( makeFileSectionName( this.currentFilename, fileInfo ) );
157         sink.sectionTitle2_();
158 
159         sink.table();
160         sink.tableRow();
161         sink.tableHeaderCell();
162         sink.text( bundle.getString( "report.pmd.column.violation" ) );
163         sink.tableHeaderCell_();
164         if ( this.renderRuleViolationPriority )
165         {
166             sink.tableHeaderCell();
167             sink.text( bundle.getString( "report.pmd.column.priority" ) );
168             sink.tableHeaderCell_();
169         }
170         sink.tableHeaderCell();
171         sink.text( bundle.getString( "report.pmd.column.line" ) );
172         sink.tableHeaderCell_();
173         sink.tableRow_();
174     }
175 
176     private void endFileSection()
177     {
178         sink.table_();
179         sink.section2_();
180     }
181 
182     private void processSingleRuleViolation( RuleViolation ruleViolation, PmdFileInfo fileInfo )
183     {
184         sink.tableRow();
185         sink.tableCell();
186         sink.text( ruleViolation.getDescription() );
187         sink.tableCell_();
188 
189         if ( this.renderRuleViolationPriority )
190         {
191             sink.tableCell();
192             sink.text( String.valueOf( ruleViolation.getRule().getPriority().getPriority() ) );
193             sink.tableCell_();
194         }
195 
196         sink.tableCell();
197 
198         int beginLine = ruleViolation.getBeginLine();
199         outputLineLink( beginLine, fileInfo );
200         int endLine = ruleViolation.getEndLine();
201         if ( endLine != beginLine )
202         {
203             sink.text( "&#x2013;" );
204             outputLineLink( endLine, fileInfo );
205         }
206 
207         sink.tableCell_();
208         sink.tableRow_();
209     }
210 
211     // PMD might run the analysis multi-threaded, so the violations might be reported
212     // out of order. We sort them here by filename and line number before writing them to
213     // the report.
214     private void processViolations()
215         throws IOException
216     {
217         sink.section1();
218         sink.sectionTitle1();
219         sink.text( bundle.getString( "report.pmd.files" ) );
220         sink.sectionTitle1_();
221 
222         // TODO files summary
223 
224         fileCount = files.size();
225         List<RuleViolation> violations2 = new ArrayList<>( violations );
226         Collections.sort( violations2, new Comparator<RuleViolation>()
227         {
228             /** {@inheritDoc} */
229             public int compare( RuleViolation o1, RuleViolation o2 )
230             {
231                 int filenames = o1.getFilename().compareTo( o2.getFilename() );
232                 if ( filenames == 0 )
233                 {
234                     return o1.getBeginLine() - o2.getBeginLine();
235                 }
236                 else
237                 {
238                     return filenames;
239                 }
240             }
241         } );
242 
243         boolean fileSectionStarted = false;
244         String previousFilename = null;
245         for ( RuleViolation ruleViolation : violations2 )
246         {
247             String currentFn = ruleViolation.getFilename();
248             PmdFileInfo fileInfo = determineFileInfo( currentFn );
249 
250             if ( !currentFn.equalsIgnoreCase( previousFilename ) && fileSectionStarted )
251             {
252                 endFileSection();
253                 fileSectionStarted = false;
254             }
255             if ( !fileSectionStarted )
256             {
257                 startFileSection( currentFn, fileInfo );
258                 fileSectionStarted = true;
259             }
260 
261             processSingleRuleViolation( ruleViolation, fileInfo );
262 
263             previousFilename = currentFn;
264         }
265 
266         if ( fileSectionStarted )
267         {
268             endFileSection();
269         }
270 
271         if ( fileCount == 0 )
272         {
273             sink.paragraph();
274             sink.text( bundle.getString( "report.pmd.noProblems" ) );
275             sink.paragraph_();
276         }
277 
278         sink.section1_();
279     }
280 
281     private void outputLineLink( int line, PmdFileInfo fileInfo )
282     {
283         String xrefLocation = null;
284         if ( fileInfo != null )
285         {
286             xrefLocation = fileInfo.getXrefLocation();
287         }
288 
289         if ( xrefLocation != null )
290         {
291             sink.link( xrefLocation + "/" + currentFilename.replaceAll( "\\.java$", ".html" ) + "#L" + line );
292         }
293         sink.text( String.valueOf( line ) );
294         if ( xrefLocation != null )
295         {
296             sink.link_();
297         }
298     }
299 
300     private void processProcessingErrors() throws IOException
301     {
302         // sort the problem by filename first, since PMD is executed multi-threaded
303         // and might reports the results unsorted
304         Collections.sort( processingErrors, new Comparator<ProcessingError>()
305         {
306             @Override
307             public int compare( ProcessingError e1, ProcessingError e2 )
308             {
309                 return e1.getFile().compareTo( e2.getFile() );
310             }
311         } );
312 
313         sink.section1();
314         sink.sectionTitle1();
315         sink.text( bundle.getString( "report.pmd.processingErrors.title" ) );
316         sink.sectionTitle1_();
317 
318         sink.table();
319         sink.tableRow();
320         sink.tableHeaderCell();
321         sink.text( bundle.getString( "report.pmd.processingErrors.column.filename" ) );
322         sink.tableHeaderCell_();
323         sink.tableHeaderCell();
324         sink.text( bundle.getString( "report.pmd.processingErrors.column.problem" ) );
325         sink.tableHeaderCell_();
326         sink.tableRow_();
327 
328         for ( ProcessingError error : processingErrors )
329         {
330             processSingleProcessingError( error );
331         }
332 
333         sink.table_();
334 
335         sink.section1_();
336     }
337 
338     private void processSingleProcessingError( ProcessingError error ) throws IOException
339     {
340         String filename = error.getFile();
341         PmdFileInfo fileInfo = determineFileInfo( filename );
342         filename = makeFileSectionName( shortenFilename( filename, fileInfo ), fileInfo );
343 
344         sink.tableRow();
345         sink.tableCell();
346         sink.text( filename );
347         sink.tableCell_();
348         sink.tableCell();
349         sink.text( error.getMsg() );
350         sink.verbatim( null );
351         sink.rawText( error.getDetail() );
352         sink.verbatim_();
353         sink.tableCell_();
354         sink.tableRow_();
355     }
356 
357     public void beginDocument()
358     {
359         sink.head();
360         sink.title();
361         sink.text( getTitle() );
362         sink.title_();
363         sink.head_();
364 
365         sink.body();
366 
367         sink.section1();
368         sink.sectionTitle1();
369         sink.text( getTitle() );
370         sink.sectionTitle1_();
371 
372         sink.paragraph();
373         sink.text( bundle.getString( "report.pmd.pmdlink" ) + " " );
374         sink.link( "http://pmd.sourceforge.net/" );
375         sink.text( "PMD" );
376         sink.link_();
377         sink.text( " " + AbstractPmdReport.getPmdVersion() + "." );
378         sink.paragraph_();
379 
380         sink.section1_();
381 
382         // TODO overall summary
383     }
384 
385     /*
386      * private void processMetrics() { if ( metrics.size() == 0 ) { return; } sink.section1(); sink.sectionTitle1();
387      * sink.text( "Metrics" ); sink.sectionTitle1_(); sink.table(); sink.tableRow(); sink.tableHeaderCell(); sink.text(
388      * "Name" ); sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Count" ); sink.tableHeaderCell_();
389      * sink.tableHeaderCell(); sink.text( "High" ); sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Low" );
390      * sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Average" ); sink.tableHeaderCell_();
391      * sink.tableRow_(); for ( Metric met : metrics ) { sink.tableRow(); sink.tableCell(); sink.text(
392      * met.getMetricName() ); sink.tableCell_(); sink.tableCell(); sink.text( String.valueOf( met.getCount() ) );
393      * sink.tableCell_(); sink.tableCell(); sink.text( String.valueOf( met.getHighValue() ) ); sink.tableCell_();
394      * sink.tableCell(); sink.text( String.valueOf( met.getLowValue() ) ); sink.tableCell_(); sink.tableCell();
395      * sink.text( String.valueOf( met.getAverage() ) ); sink.tableCell_(); sink.tableRow_(); } sink.table_();
396      * sink.section1_(); }
397      */
398 
399     public void render()
400         throws IOException
401     {
402         processViolations();
403 
404         if ( !processingErrors.isEmpty() )
405         {
406             processProcessingErrors();
407         }
408     }
409 
410     public void endDocument()
411         throws IOException
412     {
413         // The Metrics report useless with the current PMD metrics impl.
414         // For instance, run the coupling ruleset and you will get a boatload
415         // of excessive imports metrics, none of which is really any use.
416         // TODO Determine if we are going to just ignore metrics.
417 
418         // processMetrics();
419 
420         sink.body_();
421 
422         sink.flush();
423 
424         sink.close();
425     }
426 
427     public void setFiles( Map<File, PmdFileInfo> files )
428     {
429         this.files = files;
430     }
431 
432     public void setRenderRuleViolationPriority( boolean renderRuleViolationPriority )
433     {
434         this.renderRuleViolationPriority = renderRuleViolationPriority;
435     }
436 }