1 package org.apache.maven.plugins.pmd;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
45
46
47
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
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
107
108
109
110
111
112
113
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
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( "–" );
228 outputLineLink( endLine, fileInfo );
229 }
230
231 sink.tableCell_();
232 sink.tableRow_();
233 }
234
235
236
237
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
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
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
383
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
463 }
464
465
466
467
468
469
470
471
472
473
474
475
476
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
505
506
507
508
509
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 }