• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html
3 /**
4 *******************************************************************************
5 * Copyright (C) 2004-2013, International Business Machines Corporation and    *
6 * others. All Rights Reserved.                                                *
7 *******************************************************************************
8 */
9 
10 /**
11  * Compare two API files (generated by GatherAPIData) and generate a report
12  * on the differences.
13  *
14  * Sample invocation:
15  * java -old: icu4j28.api.zip -new: icu4j30.api -html -out: icu4j_compare_28_30.html
16  *
17  * TODO:
18  * - make 'changed apis' smarter - detect method parameter or return type change
19  *   for this, the sequential search through methods ordered by signature won't do.
20  *     We need to gather all added and removed overloads for a method, and then
21  *     compare all added against all removed in order to identify this kind of
22  *     change.
23  */
24 
25 package com.ibm.icu.dev.tool.docs;
26 
27 import java.io.BufferedWriter;
28 import java.io.FileNotFoundException;
29 import java.io.FileOutputStream;
30 import java.io.OutputStream;
31 import java.io.OutputStreamWriter;
32 import java.io.PrintWriter;
33 import java.io.UnsupportedEncodingException;
34 import java.text.DateFormat;
35 import java.text.SimpleDateFormat;
36 import java.util.ArrayList;
37 import java.util.Collection;
38 import java.util.Comparator;
39 import java.util.Date;
40 import java.util.HashSet;
41 import java.util.Iterator;
42 import java.util.Set;
43 import java.util.TreeSet;
44 
45 public class ReportAPI {
46     APIData oldData;
47     APIData newData;
48     boolean html;
49     String outputFile;
50 
51     TreeSet<APIInfo> added;
52     TreeSet<APIInfo> removed;
53     TreeSet<APIInfo> promotedStable;
54     TreeSet<APIInfo> promotedDraft;
55     TreeSet<APIInfo> obsoleted;
56     ArrayList<DeltaInfo> changed;
57 
58     static final class DeltaInfo extends APIInfo {
59         APIInfo added;
60         APIInfo removed;
61 
DeltaInfo(APIInfo added, APIInfo removed)62         DeltaInfo(APIInfo added, APIInfo removed) {
63             this.added = added;
64             this.removed = removed;
65         }
66 
67         @Override
getVal(int typ)68         public int getVal(int typ) {
69             return added.getVal(typ);
70         }
71 
72         @Override
get(int typ, boolean brief)73         public String get(int typ, boolean brief) {
74             return added.get(typ, brief);
75         }
76 
77         @Override
print(PrintWriter pw, boolean detail, boolean html)78         public void print(PrintWriter pw, boolean detail, boolean html) {
79             pw.print("    ");
80             removed.print(pw, detail, html);
81             if (html) {
82                 pw.println("</br>");
83             } else {
84                 pw.println();
85                 pw.print("--> ");
86             }
87             added.print(pw, detail, html);
88         }
89     }
90 
main(String[] args)91     public static void main(String[] args) {
92         String oldFile = null;
93         String newFile = null;
94         String outFile = null;
95         boolean html = false;
96         boolean internal = false;
97         for (int i = 0; i < args.length; ++i) {
98             String arg = args[i];
99             if (arg.equals("-old:")) {
100                 oldFile = args[++i];
101             } else if (arg.equals("-new:")) {
102                 newFile = args[++i];
103             } else if (arg.equals("-out:")) {
104                 outFile = args[++i];
105             } else if (arg.equals("-html")) {
106                 html = true;
107             } else if (arg.equals("-internal")) {
108                 internal = true;
109             }
110         }
111 
112         new ReportAPI(oldFile, newFile, internal).writeReport(outFile, html, internal);
113     }
114 
115     /*
116       while the both are methods and the class and method names are the same, collect
117       overloads.  when you hit a new method or class, compare the overloads
118       looking for the same # of params and simple param changes.  ideally
119       there are just a few.
120 
121       String oldA = null;
122       String oldR = null;
123       if (!a.isMethod()) {
124       remove and continue
125       }
126       String am = a.getClassName() + "." + a.getName();
127       String rm = r.getClassName() + "." + r.getName();
128       int comp = am.compare(rm);
129       if (comp == 0 && a.isMethod() && r.isMethod())
130 
131     */
132 
ReportAPI(String oldFile, String newFile, boolean internal)133     ReportAPI(String oldFile, String newFile, boolean internal) {
134         this(APIData.read(oldFile, internal), APIData.read(newFile, internal));
135     }
136 
ReportAPI(APIData oldData, APIData newData)137     ReportAPI(APIData oldData, APIData newData) {
138         this.oldData = oldData;
139         this.newData = newData;
140 
141         removed = (TreeSet<APIInfo>)oldData.set.clone();
142         removed.removeAll(newData.set);
143 
144         added = (TreeSet<APIInfo>)newData.set.clone();
145         added.removeAll(oldData.set);
146 
147         changed = new ArrayList<DeltaInfo>();
148         Iterator<APIInfo> ai = added.iterator();
149         Iterator<APIInfo> ri = removed.iterator();
150         Comparator<APIInfo> c = APIInfo.changedComparator();
151 
152         ArrayList<APIInfo> ams = new ArrayList<APIInfo>();
153         ArrayList<APIInfo> rms = new ArrayList<APIInfo>();
154         //PrintWriter outpw = new PrintWriter(System.out);
155 
156         APIInfo a = null, r = null;
157         while ((a != null || ai.hasNext()) && (r != null || ri.hasNext())) {
158             if (a == null) a = ai.next();
159             if (r == null) r = ri.next();
160 
161             String am = a.getClassName() + "." + a.getName();
162             String rm = r.getClassName() + "." + r.getName();
163             int comp = am.compareTo(rm);
164             if (comp == 0 && a.isMethod() && r.isMethod()) { // collect overloads
165                 ams.add(a); a = null;
166                 rms.add(r); r = null;
167                 continue;
168             }
169 
170             if (!ams.isEmpty()) {
171                 // simplest case first
172                 if (ams.size() == 1 && rms.size() == 1) {
173                     changed.add(new DeltaInfo(ams.get(0), rms.get(0)));
174                 } else {
175                     // dang, what to do now?
176                     // TODO: modify deltainfo to deal with lists of added and removed
177                 }
178                 ams.clear();
179                 rms.clear();
180             }
181 
182             int result = c.compare(a, r);
183             if (result < 0) {
184                 a = null;
185             } else if (result > 0) {
186                 r = null;
187             } else {
188                 changed.add(new DeltaInfo(a, r));
189                 a = null;
190                 r = null;
191             }
192         }
193 
194         // now clean up added and removed by cleaning out the changed members
195         Iterator<DeltaInfo> ci = changed.iterator();
196         while (ci.hasNext()) {
197             DeltaInfo di = ci.next();
198             added.remove(di.added);
199             removed.remove(di.removed);
200         }
201 
202         Set<APIInfo> tempAdded = new HashSet<APIInfo>();
203         tempAdded.addAll(newData.set);
204         tempAdded.removeAll(removed);
205         TreeSet<APIInfo> changedAdded = new TreeSet<APIInfo>(APIInfo.defaultComparator());
206         changedAdded.addAll(tempAdded);
207 
208         Set<APIInfo> tempRemoved = new HashSet<APIInfo>();
209         tempRemoved.addAll(oldData.set);
210         tempRemoved.removeAll(added);
211         TreeSet<APIInfo> changedRemoved = new TreeSet<APIInfo>(APIInfo.defaultComparator());
212         changedRemoved.addAll(tempRemoved);
213 
214         promotedStable = new TreeSet<APIInfo>(APIInfo.defaultComparator());
215         promotedDraft = new TreeSet<APIInfo>(APIInfo.defaultComparator());
216         obsoleted = new TreeSet<APIInfo>(APIInfo.defaultComparator());
217         ai = changedAdded.iterator();
218         ri = changedRemoved.iterator();
219         a = r = null;
220         while ((a != null || ai.hasNext()) && (r != null || ri.hasNext())) {
221             if (a == null) a = ai.next();
222             if (r == null) r = ri.next();
223             int result = c.compare(a, r);
224             if (result < 0) {
225                 a = null;
226             } else if (result > 0) {
227                 r = null;
228             } else {
229                 int change = statusChange(a, r);
230                 if (change > 0) {
231                     if (a.isStable()) {
232                         promotedStable.add(a);
233                     } else {
234                         promotedDraft.add(a);
235                     }
236                 } else if (change < 0) {
237                     obsoleted.add(a);
238                 }
239                 a = null;
240                 r = null;
241             }
242         }
243 
244         added = stripAndResort(added);
245         removed = stripAndResort(removed);
246         promotedStable = stripAndResort(promotedStable);
247         promotedDraft = stripAndResort(promotedDraft);
248         obsoleted = stripAndResort(obsoleted);
249     }
250 
statusChange(APIInfo lhs, APIInfo rhs)251     private int statusChange(APIInfo lhs, APIInfo rhs) { // new. old
252         for (int i = 0; i < APIInfo.NUM_TYPES; ++i) {
253             if (lhs.get(i, true).equals(rhs.get(i, true)) == (i == APIInfo.STA)) {
254                 return 0;
255             }
256         }
257         int lstatus = lhs.getVal(APIInfo.STA);
258         if (lstatus == APIInfo.STA_OBSOLETE
259             || lstatus == APIInfo.STA_DEPRECATED
260             || lstatus == APIInfo.STA_INTERNAL) {
261             return -1;
262         }
263         return 1;
264     }
265 
writeReport(String outFile, boolean html, boolean internal)266     private boolean writeReport(String outFile, boolean html, boolean internal) {
267         OutputStream os = System.out;
268         if (outFile != null) {
269             try {
270                 os = new FileOutputStream(outFile);
271             }
272             catch (FileNotFoundException e) {
273                 RuntimeException re = new RuntimeException(e.getMessage());
274                 re.initCause(e);
275                 throw re;
276             }
277         }
278 
279         PrintWriter pw = null;
280         try {
281             pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os, "UTF-8")));
282         }
283         catch (UnsupportedEncodingException e) {
284             throw new IllegalStateException(); // UTF-8 should always be supported
285         }
286 
287         // Change names to remove minor, milli, and micro version numbers for the report.
288         String oldName = oldData.name;
289         int ptIndex = oldName.indexOf('.');
290         if (ptIndex >= 0) {
291             oldName = oldName.substring(0, ptIndex);
292         }
293         String newName = newData.name;
294         ptIndex = newName.indexOf('.');
295         if (ptIndex >= 0) {
296             newName = newName.substring(0, ptIndex);
297         }
298 
299 
300         DateFormat fmt = new SimpleDateFormat("yyyy");
301         String year = fmt.format(new Date());
302         String title = "ICU4J API Comparison: " + oldName + " with " + newName;
303         String info = "Contents generated by ReportAPI tool on " + new Date().toString();
304         String copyright = "© " + year + " and later: Unicode, Inc. and others."
305                 + " License & terms of use: <a href=\"http://www.unicode.org/copyright.html\">"
306                 + "http://www.unicode.org/copyright.html</a>";
307 
308         if (html) {
309             pw.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
310             pw.println("<html>");
311             pw.println("<head>");
312             pw.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">");
313             pw.println("<!-- © " + year + " and later: Unicode, Inc. and others. -->");
314             pw.println("<!-- License & terms of use: http://www.unicode.org/copyright.html -->");
315             pw.println("<title>" + title + "</title>");
316             pw.println("</head>");
317             pw.println("<body>");
318 
319             pw.println("<h1>" + title + "</h1>");
320 
321             pw.println();
322             pw.println("<hr/>");
323             pw.println("<h2>Removed from " + oldName +"</h2>");
324             if (removed.size() > 0) {
325                 printResults(removed, pw, true, false);
326             } else {
327                 pw.println("<p>(no API removed)</p>");
328             }
329 
330             pw.println();
331             pw.println("<hr/>");
332             if (internal) {
333                 pw.println("<h2>Withdrawn, Deprecated, or Obsoleted in " + newName + "</h2>");
334             } else {
335                 pw.println("<h2>Deprecated or Obsoleted in " + newName + "</h2>");
336             }
337             if (obsoleted.size() > 0) {
338                 printResults(obsoleted, pw, true, false);
339             } else {
340                 pw.println("<p>(no API obsoleted)</p>");
341             }
342 
343             pw.println();
344             pw.println("<hr/>");
345             pw.println("<h2>Changed in " + newName + " (old, new)</h2>");
346             if (changed.size() > 0) {
347                 printResults(changed, pw, true, true);
348             } else {
349                 pw.println("<p>(no API changed)</p>");
350             }
351 
352             pw.println();
353             pw.println("<hr/>");
354             pw.println("<h2>Promoted to stable in " + newName + "</h2>");
355             if (promotedStable.size() > 0) {
356                 printResults(promotedStable, pw, true, false);
357             } else {
358                 pw.println("<p>(no API promoted to stable)</p>");
359             }
360 
361             if (internal) {
362                 // APIs promoted from internal to draft is reported only when
363                 // internal API check is enabled
364                 pw.println();
365                 pw.println("<hr/>");
366                 pw.println("<h2>Promoted to draft in " + newName + "</h2>");
367                 if (promotedDraft.size() > 0) {
368                     printResults(promotedDraft, pw, true, false);
369                 } else {
370                     pw.println("<p>(no API promoted to draft)</p>");
371                 }
372             }
373 
374             pw.println();
375             pw.println("<hr/>");
376             pw.println("<h2>Added in " + newName + "</h2>");
377             if (added.size() > 0) {
378                 printResults(added, pw, true, false);
379             } else {
380                 pw.println("<p>(no API added)</p>");
381             }
382 
383             pw.println("<hr/>");
384             pw.println("<p><i><font size=\"-1\">" + info + "<br/>" + copyright + "</font></i></p>");
385             pw.println("</body>");
386             pw.println("</html>");
387         } else {
388             pw.println(title);
389             pw.println();
390             pw.println();
391 
392             pw.println("=== Removed from " + oldName + " ===");
393             if (removed.size() > 0) {
394                 printResults(removed, pw, false, false);
395             } else {
396                 pw.println("(no API removed)");
397             }
398 
399             pw.println();
400             pw.println();
401             if (internal) {
402                 pw.println("=== Withdrawn, Deprecated, or Obsoleted in " + newName + " ===");
403             } else {
404                 pw.println("=== Deprecated or Obsoleted in " + newName + " ===");
405             }
406             if (obsoleted.size() > 0) {
407                 printResults(obsoleted, pw, false, false);
408             } else {
409                 pw.println("(no API obsoleted)");
410             }
411 
412             pw.println();
413             pw.println();
414             pw.println("=== Changed in " + newName + " (old, new) ===");
415             if (changed.size() > 0) {
416                 printResults(changed, pw, false, true);
417             } else {
418                 pw.println("(no API changed)");
419             }
420 
421             pw.println();
422             pw.println();
423             pw.println("=== Promoted to stable in " + newName + " ===");
424             if (promotedStable.size() > 0) {
425                 printResults(promotedStable, pw, false, false);
426             } else {
427                 pw.println("(no API promoted to stable)");
428             }
429 
430             if (internal) {
431                 pw.println();
432                 pw.println();
433                 pw.println("=== Promoted to draft in " + newName + " ===");
434                 if (promotedDraft.size() > 0) {
435                     printResults(promotedDraft, pw, false, false);
436                 } else {
437                     pw.println("(no API promoted to draft)");
438                 }
439             }
440 
441             pw.println();
442             pw.println();
443             pw.println("=== Added in " + newName + " ===");
444             if (added.size() > 0) {
445                 printResults(added, pw, false, false);
446             } else {
447                 pw.println("(no API added)");
448             }
449 
450             pw.println();
451             pw.println("================");
452             pw.println(info);
453             pw.println(copyright);
454         }
455         pw.close();
456 
457         return false;
458     }
459 
printResults(Collection<? extends APIInfo> c, PrintWriter pw, boolean html, boolean isChangedAPIs)460     private static void printResults(Collection<? extends APIInfo> c, PrintWriter pw, boolean html,
461                                      boolean isChangedAPIs) {
462         Iterator<? extends APIInfo> iter = c.iterator();
463         String pack = null;
464         String clas = null;
465         while (iter.hasNext()) {
466             APIInfo info = iter.next();
467 
468             String packageName = info.getPackageName();
469             if (!packageName.equals(pack)) {
470                 if (html) {
471                     if (clas != null) {
472                         pw.println("</ul>");
473                     }
474                     if (pack != null) {
475                         pw.println("</ul>");
476                     }
477                     pw.println();
478                     pw.println("<h3>Package " + packageName + "</h3>");
479                     pw.print("<ul>");
480                 } else {
481                     if (pack != null) {
482                         pw.println();
483                     }
484                     pw.println();
485                     pw.println("Package " + packageName + ":");
486                 }
487                 pw.println();
488 
489                 pack = packageName;
490                 clas = null;
491             }
492 
493             if (!info.isClass() && !info.isEnum()) {
494                 String className = info.getClassName();
495                 if (!className.equals(clas)) {
496                     if (html) {
497                         if (clas != null) {
498                             pw.println("</ul>");
499                         }
500                         pw.println(className);
501                         pw.println("<ul>");
502                     } else {
503                         pw.println(className);
504                     }
505                     clas = className;
506                 }
507             }
508 
509             if (html) {
510                 pw.print("<li>");
511                 info.print(pw, isChangedAPIs, html);
512                 pw.println("</li>");
513             } else {
514                 info.println(pw, isChangedAPIs, html);
515             }
516         }
517 
518         if (html) {
519             if (clas != null) {
520                 pw.println("</ul>");
521             }
522             if (pack != null) {
523                 pw.println("</ul>");
524             }
525         }
526         pw.println();
527     }
528 
stripAndResort(TreeSet<APIInfo> t)529     private static TreeSet<APIInfo> stripAndResort(TreeSet<APIInfo> t) {
530         stripClassInfo(t);
531         TreeSet<APIInfo> r = new TreeSet<APIInfo>(APIInfo.classFirstComparator());
532         r.addAll(t);
533         return r;
534     }
535 
stripClassInfo(Collection<APIInfo> c)536     private static void stripClassInfo(Collection<APIInfo> c) {
537         // c is sorted with class info first
538         Iterator<? extends APIInfo> iter = c.iterator();
539         String cname = null;
540         while (iter.hasNext()) {
541             APIInfo info = iter.next();
542             String className = info.getClassName();
543             if (cname != null) {
544                 if (cname.equals(className)) {
545                     iter.remove();
546                     continue;
547                 }
548                 cname = null;
549             }
550             if (info.isClass()) {
551                 cname = info.getName();
552             }
553         }
554     }
555 }
556