View Javadoc
1   package org.codehaus.mojo.clirr;
2   
3   /*
4    * Copyright 2006 The Codehaus
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
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   * Generate the Clirr report.
36   *
37   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
38   */
39  public class ClirrReportGenerator
40  {
41  
42      /**
43       * Clirr's difference types.
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           // set default values that can be overwritten later
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             // TODO: differentiate source and binary? The only difference seems to be MSG_CONSTANT_REMOVED at this point
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                     // MCLIRR-18 Special handling of links to inner classes:
396                     // We link to the page for the containing class
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      * Use a null object to avoid doing null checks everywhere.
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 }