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