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.tableRow();
170         sink.tableHeaderCell();
171         sink.text( bundle.getString( "report.pmd.column.rule" ) );
172         sink.tableHeaderCell_();
173         sink.tableHeaderCell();
174         sink.text( bundle.getString( "report.pmd.column.violation" ) );
175         sink.tableHeaderCell_();
176         if ( this.renderRuleViolationPriority )
177         {
178             sink.tableHeaderCell();
179             sink.text( bundle.getString( "report.pmd.column.priority" ) );
180             sink.tableHeaderCell_();
181         }
182         sink.tableHeaderCell();
183         sink.text( bundle.getString( "report.pmd.column.line" ) );
184         sink.tableHeaderCell_();
185         sink.tableRow_();
186     }
187 
188     private void endFileSection( int level )
189     {
190         sink.table_();
191         sink.section_( level );
192     }
193 
194     private void addRuleName( Violation ruleViolation )
195     {
196         boolean hasUrl = StringUtils.isNotBlank( ruleViolation.getExternalInfoUrl() );
197 
198         if ( hasUrl )
199         {
200             sink.link( ruleViolation.getExternalInfoUrl() );
201         }
202 
203         sink.text( ruleViolation.getRule() );
204 
205         if ( hasUrl )
206         {
207             sink.link_();
208         }
209     }
210 
211     private void processSingleRuleViolation( Violation ruleViolation, PmdFileInfo fileInfo )
212     {
213         sink.tableRow();
214         sink.tableCell();
215         addRuleName( ruleViolation );
216         sink.tableCell_();
217         sink.tableCell();
218         sink.text( ruleViolation.getText() );
219         sink.tableCell_();
220 
221         if ( this.renderRuleViolationPriority )
222         {
223             sink.tableCell();
224             sink.text( String.valueOf( RulePriority.valueOf( ruleViolation.getPriority() ).getPriority() ) );
225             sink.tableCell_();
226         }
227 
228         sink.tableCell();
229 
230         int beginLine = ruleViolation.getBeginline();
231         outputLineLink( beginLine, fileInfo );
232         int endLine = ruleViolation.getEndline();
233         if ( endLine != beginLine )
234         {
235             sink.text( "&#x2013;" ); // \u2013 is a medium long dash character
236             outputLineLink( endLine, fileInfo );
237         }
238 
239         sink.tableCell_();
240         sink.tableRow_();
241     }
242 
243     // PMD might run the analysis multi-threaded, so the violations might be reported
244     // out of order. We sort them here by filename and line number before writing them to
245     // the report.
246     private void renderViolations()
247         throws IOException
248     {
249         sink.section1();
250         sink.sectionTitle1();
251         sink.text( bundle.getString( "report.pmd.files" ) );
252         sink.sectionTitle1_();
253 
254         // TODO files summary
255 
256         List<Violation> violations2 = new ArrayList<>( violations );
257         renderViolationsTable( 2, violations2 );
258 
259         sink.section1_();
260     }
261 
262     private void renderViolationsByPriority() throws IOException
263     {
264         if ( !renderViolationsByPriority )
265         {
266             return;
267         }
268 
269         boolean oldPriorityColumn = this.renderRuleViolationPriority;
270         this.renderRuleViolationPriority = false;
271 
272         sink.section1();
273         sink.sectionTitle1();
274         sink.text( bundle.getString( "report.pmd.violationsByPriority" ) );
275         sink.sectionTitle1_();
276 
277         Map<RulePriority, List<Violation>> violationsByPriority = new HashMap<>();
278         for ( Violation violation : violations )
279         {
280             RulePriority priority = RulePriority.valueOf( violation.getPriority() );
281             List<Violation> violationSegment = violationsByPriority.get( priority );
282             if ( violationSegment == null )
283             {
284                 violationSegment = new ArrayList<>();
285                 violationsByPriority.put( priority, violationSegment );
286             }
287             violationSegment.add( violation );
288         }
289 
290         for ( RulePriority priority : RulePriority.values() )
291         {
292             List<Violation> violationsWithPriority = violationsByPriority.get( priority );
293             if ( violationsWithPriority == null || violationsWithPriority.isEmpty() )
294             {
295                 continue;
296             }
297 
298             sink.section2();
299             sink.sectionTitle2();
300             sink.text( bundle.getString( "report.pmd.priority" ) + " " + priority.getPriority() );
301             sink.sectionTitle2_();
302 
303             renderViolationsTable( 3, violationsWithPriority );
304 
305             sink.section2_();
306         }
307 
308         if ( violations.isEmpty() )
309         {
310             sink.paragraph();
311             sink.text( bundle.getString( "report.pmd.noProblems" ) );
312             sink.paragraph_();
313         }
314 
315         sink.section1_();
316 
317         this.renderRuleViolationPriority = oldPriorityColumn;
318     }
319 
320     private void renderViolationsTable( int level, List<Violation> violationSegment )
321     throws IOException
322     {
323         Collections.sort( violationSegment, new Comparator<Violation>()
324         {
325             /** {@inheritDoc} */
326             public int compare( Violation o1, Violation o2 )
327             {
328                 int filenames = o1.getFileName().compareTo( o2.getFileName() );
329                 if ( filenames == 0 )
330                 {
331                     return o1.getBeginline() - o2.getBeginline();
332                 }
333                 else
334                 {
335                     return filenames;
336                 }
337             }
338         } );
339 
340         boolean fileSectionStarted = false;
341         String previousFilename = null;
342         for ( Violation ruleViolation : violationSegment )
343         {
344             String currentFn = ruleViolation.getFileName();
345             PmdFileInfo fileInfo = determineFileInfo( currentFn );
346 
347             if ( !currentFn.equalsIgnoreCase( previousFilename ) && fileSectionStarted )
348             {
349                 endFileSection( level );
350                 fileSectionStarted = false;
351             }
352             if ( !fileSectionStarted )
353             {
354                 startFileSection( level, currentFn, fileInfo );
355                 fileSectionStarted = true;
356             }
357 
358             processSingleRuleViolation( ruleViolation, fileInfo );
359 
360             previousFilename = currentFn;
361         }
362 
363         if ( fileSectionStarted )
364         {
365             endFileSection( level );
366         }
367     }
368 
369     private void outputLineLink( int line, PmdFileInfo fileInfo )
370     {
371         String xrefLocation = null;
372         if ( fileInfo != null )
373         {
374             xrefLocation = fileInfo.getXrefLocation();
375         }
376 
377         if ( xrefLocation != null )
378         {
379             sink.link( xrefLocation + "/" + currentFilename.replaceAll( "\\.java$", ".html" ) + "#L" + line );
380         }
381         sink.text( String.valueOf( line ) );
382         if ( xrefLocation != null )
383         {
384             sink.link_();
385         }
386     }
387 
388     // PMD might run the analysis multi-threaded, so the suppressed violations might be reported
389     // out of order. We sort them here by filename before writing them to
390     // the report.
391     private void renderSuppressedViolations()
392         throws IOException
393     {
394         sink.section1();
395         sink.sectionTitle1();
396         sink.text( bundle.getString( "report.pmd.suppressedViolations.title" ) );
397         sink.sectionTitle1_();
398 
399         Collections.sort( suppressedViolations, new Comparator<SuppressedViolation>()
400         {
401             @Override
402             public int compare( SuppressedViolation o1, SuppressedViolation o2 )
403             {
404                 return o1.getFilename().compareTo( o2.getFilename() );
405             }
406         } );
407 
408         sink.table();
409         sink.tableRow();
410         sink.tableHeaderCell();
411         sink.text( bundle.getString( "report.pmd.suppressedViolations.column.filename" ) );
412         sink.tableHeaderCell_();
413         sink.tableHeaderCell();
414         sink.text( bundle.getString( "report.pmd.suppressedViolations.column.ruleMessage" ) );
415         sink.tableHeaderCell_();
416         sink.tableHeaderCell();
417         sink.text( bundle.getString( "report.pmd.suppressedViolations.column.suppressionType" ) );
418         sink.tableHeaderCell_();
419         sink.tableHeaderCell();
420         sink.text( bundle.getString( "report.pmd.suppressedViolations.column.userMessage" ) );
421         sink.tableHeaderCell_();
422         sink.tableRow_();
423 
424         for ( SuppressedViolation suppressedViolation : suppressedViolations )
425         {
426             String filename = suppressedViolation.getFilename();
427             PmdFileInfo fileInfo = determineFileInfo( filename );
428             filename = shortenFilename( filename, fileInfo );
429 
430             sink.tableRow();
431 
432             sink.tableCell();
433             sink.text( filename );
434             sink.tableCell_();
435 
436             sink.tableCell();
437             sink.text( suppressedViolation.getRuleMessage() );
438             sink.tableCell_();
439 
440             sink.tableCell();
441             sink.text( suppressedViolation.getSuppressionType() );
442             sink.tableCell_();
443 
444             sink.tableCell();
445             sink.text( suppressedViolation.getUserMessage() );
446             sink.tableCell_();
447 
448             sink.tableRow_();
449         }
450 
451         sink.table_();
452         sink.section1_();
453     }
454 
455     private void processProcessingErrors() throws IOException
456     {
457         // sort the problem by filename first, since PMD is executed multi-threaded
458         // and might reports the results unsorted
459         Collections.sort( processingErrors, new Comparator<ProcessingError>()
460         {
461             @Override
462             public int compare( ProcessingError e1, ProcessingError e2 )
463             {
464                 return e1.getFilename().compareTo( e2.getFilename() );
465             }
466         } );
467 
468         sink.section1();
469         sink.sectionTitle1();
470         sink.text( bundle.getString( "report.pmd.processingErrors.title" ) );
471         sink.sectionTitle1_();
472 
473         sink.table();
474         sink.tableRow();
475         sink.tableHeaderCell();
476         sink.text( bundle.getString( "report.pmd.processingErrors.column.filename" ) );
477         sink.tableHeaderCell_();
478         sink.tableHeaderCell();
479         sink.text( bundle.getString( "report.pmd.processingErrors.column.problem" ) );
480         sink.tableHeaderCell_();
481         sink.tableRow_();
482 
483         for ( ProcessingError error : processingErrors )
484         {
485             processSingleProcessingError( error );
486         }
487 
488         sink.table_();
489 
490         sink.section1_();
491     }
492 
493     private void processSingleProcessingError( ProcessingError error ) throws IOException
494     {
495         String filename = error.getFilename();
496         PmdFileInfo fileInfo = determineFileInfo( filename );
497         filename = makeFileSectionName( shortenFilename( filename, fileInfo ), fileInfo );
498 
499         sink.tableRow();
500         sink.tableCell();
501         sink.text( filename );
502         sink.tableCell_();
503         sink.tableCell();
504         sink.text( error.getMsg() );
505         sink.verbatim( null );
506         sink.rawText( error.getDetail() );
507         sink.verbatim_();
508         sink.tableCell_();
509         sink.tableRow_();
510     }
511 
512     public void beginDocument()
513     {
514         sink.head();
515         sink.title();
516         sink.text( getTitle() );
517         sink.title_();
518         sink.head_();
519 
520         sink.body();
521 
522         sink.section1();
523         sink.sectionTitle1();
524         sink.text( getTitle() );
525         sink.sectionTitle1_();
526 
527         sink.paragraph();
528         sink.text( bundle.getString( "report.pmd.pmdlink" ) + " " );
529         sink.link( "https://pmd.github.io" );
530         sink.text( "PMD" );
531         sink.link_();
532         sink.text( " " + AbstractPmdReport.getPmdVersion() + "." );
533         sink.paragraph_();
534 
535         sink.section1_();
536 
537         // TODO overall summary
538     }
539 
540     /*
541      * private void processMetrics() { if ( metrics.size() == 0 ) { return; } sink.section1(); sink.sectionTitle1();
542      * sink.text( "Metrics" ); sink.sectionTitle1_(); sink.table(); sink.tableRow(); sink.tableHeaderCell(); sink.text(
543      * "Name" ); sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Count" ); sink.tableHeaderCell_();
544      * sink.tableHeaderCell(); sink.text( "High" ); sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Low" );
545      * sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Average" ); sink.tableHeaderCell_();
546      * sink.tableRow_(); for ( Metric met : metrics ) { sink.tableRow(); sink.tableCell(); sink.text(
547      * met.getMetricName() ); sink.tableCell_(); sink.tableCell(); sink.text( String.valueOf( met.getCount() ) );
548      * sink.tableCell_(); sink.tableCell(); sink.text( String.valueOf( met.getHighValue() ) ); sink.tableCell_();
549      * sink.tableCell(); sink.text( String.valueOf( met.getLowValue() ) ); sink.tableCell_(); sink.tableCell();
550      * sink.text( String.valueOf( met.getAverage() ) ); sink.tableCell_(); sink.tableRow_(); } sink.table_();
551      * sink.section1_(); }
552      */
553 
554     public void render()
555         throws IOException
556     {
557         if ( !violations.isEmpty() )
558         {
559             renderViolationsByPriority();
560 
561             renderViolations();
562         }
563         else
564         {
565             sink.paragraph();
566             sink.text( bundle.getString( "report.pmd.noProblems" ) );
567             sink.paragraph_();
568         }
569 
570         if ( !suppressedViolations.isEmpty() )
571         {
572             renderSuppressedViolations();
573         }
574 
575         if ( !processingErrors.isEmpty() )
576         {
577             processProcessingErrors();
578         }
579     }
580 
581     public void endDocument()
582         throws IOException
583     {
584         // The Metrics report useless with the current PMD metrics impl.
585         // For instance, run the coupling ruleset and you will get a boatload
586         // of excessive imports metrics, none of which is really any use.
587         // TODO Determine if we are going to just ignore metrics.
588 
589         // processMetrics();
590 
591         sink.body_();
592 
593         sink.flush();
594 
595         sink.close();
596     }
597 
598     public void setFiles( Map<File, PmdFileInfo> files )
599     {
600         this.files = files;
601     }
602 
603     public void setRenderRuleViolationPriority( boolean renderRuleViolationPriority )
604     {
605         this.renderRuleViolationPriority = renderRuleViolationPriority;
606     }
607 
608     public void setRenderViolationsByPriority( boolean renderViolationsByPriority )
609     {
610         this.renderViolationsByPriority = renderViolationsByPriority;
611     }
612 }