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