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