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