1 package org.codehaus.mojo.clirr;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import net.sf.clirr.core.ApiDifference;
20 import net.sf.clirr.core.MessageTranslator;
21 import net.sf.clirr.core.Severity;
22
23 import org.apache.maven.doxia.sink.Sink;
24 import org.codehaus.plexus.i18n.I18N;
25
26 import java.util.*;
27 import java.util.Map.Entry;
28 import java.util.Comparator;
29 import java.util.ResourceBundle;
30 import java.util.TreeMap;
31 import java.util.regex.Pattern;
32 import java.util.regex.Matcher;
33
34
35
36
37
38
39 public class ClirrReportGenerator
40 {
41
42
43
44
45 private static final int FIELD_TYPE_CHANGED = 6004;
46 private static final int METHOD_ARGUMENT_TYPE_CHANGED = 7005;
47 private static final int METHOD_RETURN_TYPE_CHANGED = 7006;
48
49 private static final class JustificationComparator implements Comparator<Difference>
50 {
51 public int compare( Difference o1, Difference o2 )
52 {
53 return o1.getJustification().compareTo( o2.getJustification() );
54 }
55 }
56
57 private static final class ApiChangeComparator implements Comparator<ApiChange>
58 {
59 public int compare(ApiChange c1, ApiChange c2)
60 {
61 int cmp = c1.getAffectedClass().compareTo(c2.getAffectedClass());
62 if (cmp == 0)
63 {
64 cmp = c1.getFrom().compareTo(c2.getFrom());
65 if (cmp == 0)
66 {
67 return c1.getTo().compareTo(c2.getTo());
68 }
69 }
70 return cmp;
71 }
72 }
73
74 private static class ApiChange
75 {
76 private Difference difference;
77 private List<ApiDifference> apiDifferences = new LinkedList<ApiDifference>();
78 private String from;
79 private String to;
80
81 private String getAffectedClass()
82 {
83 return apiDifferences.get(0).getAffectedClass();
84 }
85
86 private String getFrom()
87 {
88 return from;
89 }
90
91 private String getTo()
92 {
93 return to;
94 }
95
96 private void computeFields()
97 {
98 ApiDifference apiDiff = apiDifferences.get(0);
99 String methodSig = apiDiff.getAffectedMethod();
100
101
102 if (apiDiff.getAffectedMethod() != null)
103 {
104 from = apiDiff.getAffectedMethod();
105 }
106 else if (apiDiff.getAffectedField() != null)
107 {
108 from = apiDiff.getAffectedField();
109 } else {
110 from = "";
111 }
112 to = difference.getTo();
113
114
115 switch (difference.getDifferenceType())
116 {
117 case FIELD_TYPE_CHANGED: {
118 String clirrReport = apiDiff.getReport(new MessageTranslator());
119 Pattern p = Pattern.compile("Changed type of field ([^ ]+) from ([^ ]+) to ([^ ]+)");
120 Matcher m = p.matcher(clirrReport);
121 if (m.find())
122 {
123 from = m.group(2) + ' ' + m.group(1);
124 to = m.group(3) + ' ' + m.group(1);
125 }
126 break;
127 }
128
129 case METHOD_ARGUMENT_TYPE_CHANGED: {
130 to = Difference.getNewMethodSignature( methodSig, apiDifferences );
131 break;
132 }
133
134 case METHOD_RETURN_TYPE_CHANGED: {
135 String clirrReport = apiDiff.getReport(new MessageTranslator());
136 Pattern p = Pattern.compile("Return type of method '[^']+' has been changed to (.+)");
137 Matcher m = p.matcher(clirrReport);
138 if (m.find())
139 {
140 int openParIdx = methodSig.indexOf('(');
141 int afterReturnTypeIdx = methodSig.lastIndexOf(' ', openParIdx);
142 int beforeReturnTypeIdx = methodSig.lastIndexOf(' ', afterReturnTypeIdx - 1);
143 to = new StringBuilder()
144 .append(methodSig, 0, beforeReturnTypeIdx + 1)
145 .append(m.group(1))
146 .append(methodSig, afterReturnTypeIdx, methodSig.length())
147 .toString();
148 }
149 break;
150 }
151
152 }
153 }
154
155 }
156
157 private final I18N i18n;
158
159 private final ResourceBundle bundle;
160
161 private final Sink sink;
162 private boolean enableSeveritySummary;
163 private final Locale locale;
164 private Severity minSeverity;
165 private String xrefLocation;
166 private String currentVersion;
167 private String comparisonVersion;
168
169 public ClirrReportGenerator( Sink sink, I18N i18n, ResourceBundle bundle, Locale locale )
170 {
171 this.i18n = i18n;
172 this.bundle = bundle;
173 this.sink = sink;
174 this.enableSeveritySummary = true;
175 this.locale = locale;
176 }
177
178 public void generateReport( ClirrDiffListener listener )
179 {
180 doHeading();
181
182 if ( enableSeveritySummary )
183 {
184 doSeveritySummary( listener );
185 }
186
187 doDetails( listener );
188 doApiChanges( listener );
189
190 sink.body_();
191 sink.flush();
192 sink.close();
193 }
194
195 private void doHeading()
196 {
197 sink.head();
198 sink.title();
199
200 String title = bundle.getString( "report.clirr.title" );
201 sink.text( title );
202 sink.title_();
203 sink.head_();
204
205 sink.body();
206
207 sink.section1();
208 sink.sectionTitle1();
209 sink.text( title );
210 sink.sectionTitle1_();
211
212 sink.paragraph();
213 sink.text( bundle.getString( "report.clirr.clirrlink" ) + " " );
214 sink.link( "http://clirr.sourceforge.net/" );
215 sink.text( "Clirr" );
216 sink.link_();
217 sink.text( "." );
218 sink.paragraph_();
219
220 sink.list();
221
222 sink.listItem();
223 sink.text( bundle.getString( "report.clirr.version.current" ) + " " );
224 sink.text( getCurrentVersion() );
225 sink.listItem_();
226
227 if ( getComparisonVersion() != null )
228 {
229 sink.listItem();
230 sink.text( bundle.getString( "report.clirr.version.comparison" ) + " " );
231 sink.text( getComparisonVersion() );
232 sink.listItem_();
233 }
234
235 sink.list_();
236
237 sink.section1_();
238 }
239
240 private void iconInfo()
241 {
242 icon( "report.clirr.level.info", "images/icon_info_sml.gif" );
243 }
244
245 private void iconWarning()
246 {
247 icon( "report.clirr.level.warning" , "images/icon_warning_sml.gif" );
248 }
249
250 private void iconError()
251 {
252 icon( "report.clirr.level.error", "images/icon_error_sml.gif" );
253 }
254
255 private void icon(String altText, String image)
256 {
257 sink.figure();
258 sink.figureCaption();
259 sink.text( bundle.getString( altText ) );
260 sink.figureCaption_();
261 sink.figureGraphics( image );
262 sink.figure_();
263 }
264
265 private void doSeveritySummary( ClirrDiffListener listener )
266 {
267 sink.section1();
268 sink.sectionTitle1();
269 sink.text( bundle.getString( "report.clirr.summary" ) );
270 sink.sectionTitle1_();
271
272 sink.table();
273
274 sink.tableRow();
275
276 sink.tableHeaderCell();
277 sink.text( bundle.getString( "report.clirr.column.severity" ) );
278 sink.tableHeaderCell_();
279
280 sink.tableHeaderCell();
281 sink.text( bundle.getString( "report.clirr.column.number" ) );
282 sink.tableHeaderCell_();
283
284 sink.tableRow_();
285
286 severityReportTableRow( listener, Severity.ERROR,
287 "report.clirr.level.error", "images/icon_error_sml.gif" );
288
289 if ( minSeverity == null || minSeverity.compareTo( Severity.WARNING ) <= 0 )
290 {
291 severityReportTableRow( listener, Severity.WARNING,
292 "report.clirr.level.warning", "images/icon_warning_sml.gif" );
293 }
294
295 if ( minSeverity == null || minSeverity.compareTo( Severity.INFO ) <= 0 )
296 {
297 severityReportTableRow( listener, Severity.INFO,
298 "report.clirr.level.info", "images/icon_info_sml.gif" );
299 }
300
301 sink.table_();
302
303 if ( minSeverity == null || minSeverity.compareTo( Severity.INFO ) > 0 )
304 {
305 sink.paragraph();
306 sink.italic();
307 sink.text( bundle.getString( "report.clirr.filtered" ) );
308 sink.italic_();
309 sink.paragraph_();
310 }
311
312 sink.section1_();
313 }
314
315 private void severityReportTableRow( ClirrDiffListener listener, Severity severity,
316 String altText, String image )
317 {
318 sink.tableRow();
319 sink.tableCell();
320 icon( altText, image );
321 sink.nonBreakingSpace();
322 sink.text( bundle.getString( altText ) );
323 sink.tableCell_();
324 sink.tableCell();
325 sink.text( String.valueOf( listener.getSeverityCount( severity ) ) );
326 sink.tableCell_();
327 sink.tableRow_();
328 }
329
330 private void doDetails( ClirrDiffListener listener )
331 {
332 sink.section1();
333 sink.sectionTitle1();
334 sink.text( bundle.getString( "report.clirr.api.incompatibilities" ) );
335 sink.sectionTitle1_();
336
337 List<ApiDifference> differences = listener.getApiDifferences();
338
339 if ( !differences.isEmpty() )
340 {
341 doIncompatibilitiesTable( differences );
342 }
343 else
344 {
345 sink.paragraph();
346 sink.text( bundle.getString( "report.clirr.noresults" ) );
347 sink.paragraph_();
348 }
349
350 sink.section1_();
351 }
352
353 private void doIncompatibilitiesTable( List<ApiDifference> differences )
354 {
355 sink.table();
356 sink.tableRow();
357 sink.tableHeaderCell();
358 sink.text( bundle.getString( "report.clirr.column.severity" ) );
359 sink.tableHeaderCell_();
360 sink.tableHeaderCell();
361 sink.text( bundle.getString( "report.clirr.column.message" ) );
362 sink.tableHeaderCell_();
363 sink.tableHeaderCell();
364 sink.text( bundle.getString( "report.clirr.column.class" ) );
365 sink.tableHeaderCell_();
366 sink.tableHeaderCell();
367 sink.text( bundle.getString( "report.clirr.column.methodorfield" ) );
368 sink.tableHeaderCell_();
369 sink.tableRow_();
370
371 MessageTranslator translator = new MessageTranslator();
372 translator.setLocale( locale );
373
374 for ( ApiDifference difference : differences )
375 {
376
377 Severity maximumSeverity = difference.getMaximumSeverity();
378
379 if ( minSeverity == null || minSeverity.compareTo( maximumSeverity ) <= 0 )
380 {
381 sink.tableRow();
382
383 sink.tableCell();
384 levelIcon( maximumSeverity );
385 sink.tableCell_();
386
387 sink.tableCell();
388 sink.text( difference.getReport( translator ) );
389 sink.tableCell_();
390
391 sink.tableCell();
392 if ( xrefLocation != null )
393 {
394 String pathToClass = difference.getAffectedClass().replace( '.', '/' );
395
396
397 final int innerClassIndex = pathToClass.lastIndexOf( '$' );
398 if ( innerClassIndex != -1 )
399 {
400 pathToClass = pathToClass.substring( 0, innerClassIndex );
401 }
402 sink.link( xrefLocation + "/" + pathToClass + ".html" );
403 }
404 sink.text( difference.getAffectedClass() );
405 if ( xrefLocation != null )
406 {
407 sink.link_();
408 }
409 sink.tableCell_();
410
411 sink.tableCell();
412 sink.text( difference.getAffectedMethod() != null ? difference.getAffectedMethod()
413 : difference.getAffectedField() );
414 sink.tableCell_();
415
416 sink.tableRow_();
417 }
418 }
419
420 sink.table_();
421 }
422
423 private void levelIcon( Severity level )
424 {
425 if ( Severity.INFO.equals( level ) )
426 {
427 iconInfo();
428 }
429 else if ( Severity.WARNING.equals( level ) )
430 {
431 iconWarning();
432 }
433 else if ( Severity.ERROR.equals( level ) )
434 {
435 iconError();
436 }
437 }
438
439 private void doApiChanges( ClirrDiffListener listener )
440 {
441 sink.section1();
442 sink.sectionTitle1();
443 sink.text( bundle.getString( "report.clirr.api.changes" ) );
444 sink.sectionTitle1_();
445
446 Map<Difference, List<ApiChange>> apiChangeReport = getApiChangeReport( listener );
447 if ( apiChangeReport.isEmpty() )
448 {
449 sink.paragraph();
450 sink.text( bundle.getString( "report.clirr.noresults" ) );
451 sink.paragraph_();
452 }
453 else
454 {
455 doApiChangesTable( apiChangeReport );
456 }
457
458 sink.section1_();
459 }
460
461 private Map<Difference, List<ApiChange>> getApiChangeReport( ClirrDiffListener listener )
462 {
463 final Map<String, List<ApiChange>> tmp = new HashMap<String, List<ApiChange>>();
464 for ( Entry<Difference, List<ApiDifference>> ignoredDiff
465 : listener.getIgnoredApiDifferences().entrySet() )
466 {
467 for ( ApiDifference apiDiff : ignoredDiff.getValue() )
468 {
469 putApiChange( tmp, apiDiff, ignoredDiff.getKey() );
470 }
471 }
472 for ( ApiDifference apiDiff : listener.getApiDifferences() )
473 {
474 putApiChange( tmp, apiDiff, null );
475 }
476
477
478 final Map<Difference, List<ApiChange>> results =
479 new TreeMap<Difference, List<ApiChange>>( new JustificationComparator() );
480
481 for ( List<ApiChange> changes : tmp.values() )
482 {
483 for ( ApiChange apiChange : changes )
484 {
485 List<ApiChange> changesForDifference = results.get( apiChange.difference );
486 if ( changesForDifference == null )
487 {
488 changesForDifference = new LinkedList<ApiChange>();
489 results.put( apiChange.difference, changesForDifference );
490 }
491 changesForDifference.add( apiChange );
492 }
493 }
494 return results;
495 }
496
497 private void putApiChange( Map<String, List<ApiChange>> results,
498 ApiDifference ignoredDiff, Difference reason )
499 {
500 String apiChangeKey = getKey( ignoredDiff );
501 List<ApiChange> apiChanges = results.get( apiChangeKey );
502 if ( apiChanges == null )
503 {
504 apiChanges = new LinkedList<ApiChange>();
505 results.put(apiChangeKey, apiChanges);
506 }
507
508 if ( reason != null && reason.getDifferenceType() == METHOD_ARGUMENT_TYPE_CHANGED )
509 {
510 ApiChange apiChange7005 = find7005ApiChange( apiChanges );
511 if ( apiChange7005 != null )
512 {
513 apiChange7005.apiDifferences.add(ignoredDiff);
514 return;
515 }
516 }
517 ApiChange change = new ApiChange();
518 change.difference = reason != null ? reason : createNullObject();
519 if (change.difference.getJustification() == null)
520 {
521 change.difference.setJustification( bundle.getString( "report.clirr.api.changes.unjustified" ) );
522 }
523 change.apiDifferences.add( ignoredDiff );
524 apiChanges.add( change );
525 }
526
527
528
529
530 private Difference createNullObject()
531 {
532 Difference difference = new Difference();
533 difference.setClassName("");
534 difference.setMethod("");
535 difference.setField("");
536 difference.setFrom("");
537 difference.setTo("");
538 difference.setJustification( bundle.getString( "report.clirr.api.changes.unjustified" ) );
539 return difference;
540 }
541
542 private String getKey( ApiDifference apiDiff )
543 {
544 if ( apiDiff.getAffectedMethod() != null )
545 {
546 return apiDiff.getAffectedClass() + " " + apiDiff.getAffectedMethod();
547 }
548 return apiDiff.getAffectedClass() + " " + apiDiff.getAffectedField();
549 }
550
551 private ApiChange find7005ApiChange( List<ApiChange> apiChanges )
552 {
553 for ( ApiChange apiChange : apiChanges )
554 {
555 if ( apiChange.difference.getDifferenceType() == METHOD_ARGUMENT_TYPE_CHANGED )
556 {
557 return apiChange;
558 }
559 }
560 return null;
561 }
562
563 private void doApiChangesTable( Map<Difference, List<ApiChange>> apiChangeReport )
564 {
565 if ( comparisonVersion != null )
566 {
567 String[] args = new String[]{comparisonVersion, currentVersion};
568 String message = i18n.format(
569 "clirr-report", locale, "report.clirr.api.changes.listing.comparisonversion", args );
570 sink.text( message );
571 }
572 else
573 {
574 sink.text( bundle.getString( "report.clirr.api.changes.listing" ) );
575 }
576
577 sink.list();
578 for ( Entry<Difference, List<ApiChange>> apiChanges
579 : apiChangeReport.entrySet() )
580 {
581 sink.listItem();
582 sink.text( apiChanges.getKey().getJustification() );
583 sink.paragraph();
584
585 sink.table();
586 sink.tableRow();
587 sink.tableHeaderCell();
588 sink.text( bundle.getString( "report.clirr.api.changes.class" ) );
589 sink.tableHeaderCell_();
590 sink.tableHeaderCell();
591 sink.text( bundle.getString( "report.clirr.api.changes.from" ) );
592 sink.tableHeaderCell_();
593 sink.tableHeaderCell();
594 sink.text( bundle.getString( "report.clirr.api.changes.to" ) );
595 sink.tableHeaderCell_();
596 sink.tableRow_();
597
598 for (ApiChange apiChange : apiChanges.getValue())
599 {
600 apiChange.computeFields();
601 }
602 Collections.sort(apiChanges.getValue(), new ApiChangeComparator());
603
604 for (ApiChange apiChange : apiChanges.getValue())
605 {
606 sink.tableRow();
607 sink.tableCell();
608 sink.text( apiChange.getAffectedClass() );
609 sink.tableCell_();
610 sink.tableCell();
611 sink.text( apiChange.getFrom() );
612 sink.tableCell_();
613 sink.tableCell();
614 sink.text( apiChange.getTo() );
615 sink.tableCell_();
616 sink.tableRow_();
617 }
618 sink.table_();
619 sink.paragraph_();
620 sink.listItem_();
621 }
622 sink.list_();
623 }
624
625 public void setEnableSeveritySummary( boolean enableSeveritySummary )
626 {
627 this.enableSeveritySummary = enableSeveritySummary;
628 }
629
630 public void setMinSeverity( Severity minSeverity )
631 {
632 this.minSeverity = minSeverity;
633 }
634
635 public String getXrefLocation()
636 {
637 return xrefLocation;
638 }
639
640 public void setXrefLocation( String xrefLocation )
641 {
642 this.xrefLocation = xrefLocation;
643 }
644
645 public String getCurrentVersion()
646 {
647 return currentVersion;
648 }
649
650 public void setCurrentVersion( String currentVersion )
651 {
652 this.currentVersion = currentVersion;
653 }
654
655 public String getComparisonVersion()
656 {
657 return comparisonVersion;
658 }
659
660 public void setComparisonVersion( String comparisonVersion )
661 {
662 this.comparisonVersion = comparisonVersion;
663 }
664 }