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