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