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