1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
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         DateFormat fmt = new SimpleDateFormat("yyyy");
288         String year = fmt.format(new Date());
289         String title = "ICU4J API Comparison: " + oldData.name + " with " + newData.name;
290         String info = "Contents generated by ReportAPI tool on " + new Date().toString();
291         String copyright = "© " + year + " and later: Unicode, Inc. and others."
292                 + " License & terms of use: <a href=\"http://www.unicode.org/copyright.html#License\">"
293                 + "http://www.unicode.org/copyright.html#License</a>";
294 
295         if (html) {
296             pw.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
297             pw.println("<html>");
298             pw.println("<head>");
299             pw.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">");
300             pw.println("<!-- © " + year + " and later: Unicode, Inc. and others. -->");
301             pw.println("<!-- License & terms of use: http://www.unicode.org/copyright.html#License -->");
302             pw.println("<title>" + title + "</title>");
303             pw.println("</head>");
304             pw.println("<body>");
305 
306             pw.println("<h1>" + title + "</h1>");
307 
308             pw.println();
309             pw.println("<hr/>");
310             pw.println("<h2>Removed from " + oldData.name +"</h2>");
311             if (removed.size() > 0) {
312                 printResults(removed, pw, true, false);
313             } else {
314                 pw.println("<p>(no API removed)</p>");
315             }
316 
317             pw.println();
318             pw.println("<hr/>");
319             if (internal) {
320                 pw.println("<h2>Withdrawn, Deprecated, or Obsoleted in " + newData.name + "</h2>");
321             } else {
322                 pw.println("<h2>Deprecated or Obsoleted in " + newData.name + "</h2>");
323             }
324             if (obsoleted.size() > 0) {
325                 printResults(obsoleted, pw, true, false);
326             } else {
327                 pw.println("<p>(no API obsoleted)</p>");
328             }
329 
330             pw.println();
331             pw.println("<hr/>");
332             pw.println("<h2>Changed in " + newData.name + " (old, new)</h2>");
333             if (changed.size() > 0) {
334                 printResults(changed, pw, true, true);
335             } else {
336                 pw.println("<p>(no API changed)</p>");
337             }
338 
339             pw.println();
340             pw.println("<hr/>");
341             pw.println("<h2>Promoted to stable in " + newData.name + "</h2>");
342             if (promotedStable.size() > 0) {
343                 printResults(promotedStable, pw, true, false);
344             } else {
345                 pw.println("<p>(no API promoted to stable)</p>");
346             }
347 
348             if (internal) {
349                 // APIs promoted from internal to draft is reported only when
350                 // internal API check is enabled
351                 pw.println();
352                 pw.println("<hr/>");
353                 pw.println("<h2>Promoted to draft in " + newData.name + "</h2>");
354                 if (promotedDraft.size() > 0) {
355                     printResults(promotedDraft, pw, true, false);
356                 } else {
357                     pw.println("<p>(no API promoted to draft)</p>");
358                 }
359             }
360 
361             pw.println();
362             pw.println("<hr/>");
363             pw.println("<h2>Added in " + newData.name + "</h2>");
364             if (added.size() > 0) {
365                 printResults(added, pw, true, false);
366             } else {
367                 pw.println("<p>(no API added)</p>");
368             }
369 
370             pw.println("<hr/>");
371             pw.println("<p><i><font size=\"-1\">" + info + "<br/>" + copyright + "</font></i></p>");
372             pw.println("</body>");
373             pw.println("</html>");
374         } else {
375             pw.println(title);
376             pw.println();
377             pw.println();
378 
379             pw.println("=== Removed from " + oldData.name + " ===");
380             if (removed.size() > 0) {
381                 printResults(removed, pw, false, false);
382             } else {
383                 pw.println("(no API removed)");
384             }
385 
386             pw.println();
387             pw.println();
388             if (internal) {
389                 pw.println("=== Withdrawn, Deprecated, or Obsoleted in " + newData.name + " ===");
390             } else {
391                 pw.println("=== Deprecated or Obsoleted in " + newData.name + " ===");
392             }
393             if (obsoleted.size() > 0) {
394                 printResults(obsoleted, pw, false, false);
395             } else {
396                 pw.println("(no API obsoleted)");
397             }
398 
399             pw.println();
400             pw.println();
401             pw.println("=== Changed in " + newData.name + " (old, new) ===");
402             if (changed.size() > 0) {
403                 printResults(changed, pw, false, true);
404             } else {
405                 pw.println("(no API changed)");
406             }
407 
408             pw.println();
409             pw.println();
410             pw.println("=== Promoted to stable in " + newData.name + " ===");
411             if (promotedStable.size() > 0) {
412                 printResults(promotedStable, pw, false, false);
413             } else {
414                 pw.println("(no API promoted to stable)");
415             }
416 
417             if (internal) {
418                 pw.println();
419                 pw.println();
420                 pw.println("=== Promoted to draft in " + newData.name + " ===");
421                 if (promotedDraft.size() > 0) {
422                     printResults(promotedDraft, pw, false, false);
423                 } else {
424                     pw.println("(no API promoted to draft)");
425                 }
426             }
427 
428             pw.println();
429             pw.println();
430             pw.println("=== Added in " + newData.name + " ===");
431             if (added.size() > 0) {
432                 printResults(added, pw, false, false);
433             } else {
434                 pw.println("(no API added)");
435             }
436 
437             pw.println();
438             pw.println("================");
439             pw.println(info);
440             pw.println(copyright);
441         }
442         pw.close();
443 
444         return false;
445     }
446 
printResults(Collection<? extends APIInfo> c, PrintWriter pw, boolean html, boolean isChangedAPIs)447     private static void printResults(Collection<? extends APIInfo> c, PrintWriter pw, boolean html,
448                                      boolean isChangedAPIs) {
449         Iterator<? extends APIInfo> iter = c.iterator();
450         String pack = null;
451         String clas = null;
452         while (iter.hasNext()) {
453             APIInfo info = iter.next();
454 
455             String packageName = info.getPackageName();
456             if (!packageName.equals(pack)) {
457                 if (html) {
458                     if (clas != null) {
459                         pw.println("</ul>");
460                     }
461                     if (pack != null) {
462                         pw.println("</ul>");
463                     }
464                     pw.println();
465                     pw.println("<h3>Package " + packageName + "</h3>");
466                     pw.print("<ul>");
467                 } else {
468                     if (pack != null) {
469                         pw.println();
470                     }
471                     pw.println();
472                     pw.println("Package " + packageName + ":");
473                 }
474                 pw.println();
475 
476                 pack = packageName;
477                 clas = null;
478             }
479 
480             if (!info.isClass() && !info.isEnum()) {
481                 String className = info.getClassName();
482                 if (!className.equals(clas)) {
483                     if (html) {
484                         if (clas != null) {
485                             pw.println("</ul>");
486                         }
487                         pw.println(className);
488                         pw.println("<ul>");
489                     } else {
490                         pw.println(className);
491                     }
492                     clas = className;
493                 }
494             }
495 
496             if (html) {
497                 pw.print("<li>");
498                 info.print(pw, isChangedAPIs, html);
499                 pw.println("</li>");
500             } else {
501                 info.println(pw, isChangedAPIs, html);
502             }
503         }
504 
505         if (html) {
506             if (clas != null) {
507                 pw.println("</ul>");
508             }
509             if (pack != null) {
510                 pw.println("</ul>");
511             }
512         }
513         pw.println();
514     }
515 
stripAndResort(TreeSet<APIInfo> t)516     private static TreeSet<APIInfo> stripAndResort(TreeSet<APIInfo> t) {
517         stripClassInfo(t);
518         TreeSet<APIInfo> r = new TreeSet<APIInfo>(APIInfo.classFirstComparator());
519         r.addAll(t);
520         return r;
521     }
522 
stripClassInfo(Collection<APIInfo> c)523     private static void stripClassInfo(Collection<APIInfo> c) {
524         // c is sorted with class info first
525         Iterator<? extends APIInfo> iter = c.iterator();
526         String cname = null;
527         while (iter.hasNext()) {
528             APIInfo info = iter.next();
529             String className = info.getClassName();
530             if (cname != null) {
531                 if (cname.equals(className)) {
532                     iter.remove();
533                     continue;
534                 }
535                 cname = null;
536             }
537             if (info.isClass()) {
538                 cname = info.getName();
539             }
540         }
541     }
542 }
543