/** ******************************************************************************* * Copyright (C) 2004-2010, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ /** * Compare two API files (generated by GatherAPIData) and generate a report * on the differences. * * Sample invocation: * java -old: icu4j28.api.zip -new: icu4j30.api -html -out: icu4j_compare_28_30.html * * TODO: * - make 'changed apis' smarter - detect method parameter or return type change * for this, the sequential search through methods ordered by signature won't do. * We need to gather all added and removed overloads for a method, and then * compare all added against all removed in order to identify this kind of * change. */ package com.ibm.icu.dev.tool.docs; import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.TreeSet; public class ReportAPI { APIData oldData; APIData newData; boolean html; String outputFile; TreeSet added; TreeSet removed; TreeSet promoted; TreeSet obsoleted; ArrayList changed; static final class DeltaInfo extends APIInfo { APIInfo added; APIInfo removed; DeltaInfo(APIInfo added, APIInfo removed) { this.added = added; this.removed = removed; } public int getVal(int typ) { return added.getVal(typ); } public String get(int typ, boolean brief) { return added.get(typ, brief); } public void print(PrintWriter pw, boolean detail, boolean html) { pw.print(" "); removed.print(pw, detail, html); if (html) { pw.println("
"); } else { pw.println(); pw.print("--> "); } added.print(pw, detail, html); } } public static void main(String[] args) { String oldFile = null; String newFile = null; String outFile = null; boolean html = false; boolean internal = false; for (int i = 0; i < args.length; ++i) { String arg = args[i]; if (arg.equals("-old:")) { oldFile = args[++i]; } else if (arg.equals("-new:")) { newFile = args[++i]; } else if (arg.equals("-out:")) { outFile = args[++i]; } else if (arg.equals("-html")) { html = true; } else if (arg.equals("-internal")) { internal = true; } } new ReportAPI(oldFile, newFile, internal).writeReport(outFile, html, internal); } /* while the both are methods and the class and method names are the same, collect overloads. when you hit a new method or class, compare the overloads looking for the same # of params and simple param changes. ideally there are just a few. String oldA = null; String oldR = null; if (!a.isMethod()) { remove and continue } String am = a.getClassName() + "." + a.getName(); String rm = r.getClassName() + "." + r.getName(); int comp = am.compare(rm); if (comp == 0 && a.isMethod() && r.isMethod()) */ ReportAPI(String oldFile, String newFile, boolean internal) { this(APIData.read(oldFile, internal), APIData.read(newFile, internal)); } ReportAPI(APIData oldData, APIData newData) { this.oldData = oldData; this.newData = newData; removed = (TreeSet)oldData.set.clone(); removed.removeAll(newData.set); added = (TreeSet)newData.set.clone(); added.removeAll(oldData.set); changed = new ArrayList(); Iterator ai = added.iterator(); Iterator ri = removed.iterator(); Comparator c = APIInfo.changedComparator(); ArrayList ams = new ArrayList(); ArrayList rms = new ArrayList(); //PrintWriter outpw = new PrintWriter(System.out); APIInfo a = null, r = null; while ((a != null || ai.hasNext()) && (r != null || ri.hasNext())) { if (a == null) a = ai.next(); if (r == null) r = ri.next(); String am = a.getClassName() + "." + a.getName(); String rm = r.getClassName() + "." + r.getName(); int comp = am.compareTo(rm); if (comp == 0 && a.isMethod() && r.isMethod()) { // collect overloads ams.add(a); a = null; rms.add(r); r = null; continue; } if (!ams.isEmpty()) { // simplest case first if (ams.size() == 1 && rms.size() == 1) { changed.add(new DeltaInfo(ams.get(0), rms.get(0))); } else { // dang, what to do now? // TODO: modify deltainfo to deal with lists of added and removed } ams.clear(); rms.clear(); } int result = c.compare(a, r); if (result < 0) { a = null; } else if (result > 0) { r = null; } else { changed.add(new DeltaInfo(a, r)); a = null; r = null; } } // now clean up added and removed by cleaning out the changed members Iterator ci = changed.iterator(); while (ci.hasNext()) { DeltaInfo di = ci.next(); added.remove(di.added); removed.remove(di.removed); } Set tempAdded = new HashSet(); tempAdded.addAll(newData.set); tempAdded.removeAll(removed); TreeSet changedAdded = new TreeSet(APIInfo.defaultComparator()); changedAdded.addAll(tempAdded); Set tempRemoved = new HashSet(); tempRemoved.addAll(oldData.set); tempRemoved.removeAll(added); TreeSet changedRemoved = new TreeSet(APIInfo.defaultComparator()); changedRemoved.addAll(tempRemoved); promoted = new TreeSet(APIInfo.defaultComparator()); obsoleted = new TreeSet(APIInfo.defaultComparator()); ai = changedAdded.iterator(); ri = changedRemoved.iterator(); a = r = null; while ((a != null || ai.hasNext()) && (r != null || ri.hasNext())) { if (a == null) a = ai.next(); if (r == null) r = ri.next(); int result = c.compare(a, r); if (result < 0) { a = null; } else if (result > 0) { r = null; } else { int change = statusChange(a, r); if (change > 0) { promoted.add(a); } else if (change < 0) { obsoleted.add(a); } a = null; r = null; } } added = stripAndResort(added); removed = stripAndResort(removed); promoted = stripAndResort(promoted); obsoleted = stripAndResort(obsoleted); } private int statusChange(APIInfo lhs, APIInfo rhs) { // new. old for (int i = 0; i < APIInfo.NUM_TYPES; ++i) { if (lhs.get(i, true).equals(rhs.get(i, true)) == (i == APIInfo.STA)) { return 0; } } int lstatus = lhs.getVal(APIInfo.STA); if (lstatus == APIInfo.STA_OBSOLETE || lstatus == APIInfo.STA_DEPRECATED || lstatus == APIInfo.STA_INTERNAL) { return -1; } return 1; } private boolean writeReport(String outFile, boolean html, boolean internal) { OutputStream os = System.out; if (outFile != null) { try { os = new FileOutputStream(outFile); } catch (FileNotFoundException e) { RuntimeException re = new RuntimeException(e.getMessage()); re.initCause(e); throw re; } } PrintWriter pw = null; try { pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os, "UTF-8"))); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(); // UTF-8 should always be supported } DateFormat fmt = new SimpleDateFormat("yyyy"); String year = fmt.format(new Date()); String title = "ICU4J API Comparison: " + oldData.name + " with " + newData.name; String info = "Contents generated by ReportAPI tool on " + new Date().toString(); String copyright = "Copyright (C) " + year + ", International Business Machines Corporation, All Rights Reserved."; if (html) { pw.println(""); pw.println(""); pw.println(""); pw.println(""); pw.println("" + title + ""); pw.println(""); pw.println(""); pw.println(""); pw.println("

" + title + "

"); pw.println(); pw.println("
"); pw.println("

Removed from " + oldData.name +"

"); if (removed.size() > 0) { printResults(removed, pw, true, false); } else { pw.println("

(no API removed)

"); } pw.println(); pw.println("
"); if (internal) { pw.println("

Withdrawn, Deprecated, or Obsoleted in " + newData.name + "

"); } else { pw.println("

Deprecated or Obsoleted in " + newData.name + "

"); } if (obsoleted.size() > 0) { printResults(obsoleted, pw, true, false); } else { pw.println("

(no API obsoleted)

"); } pw.println(); pw.println("
"); pw.println("

Changed in " + newData.name + " (old, new)

"); if (changed.size() > 0) { printResults(changed, pw, true, true); } else { pw.println("

(no API changed)

"); } pw.println(); pw.println("
"); pw.println("

Promoted to stable in " + newData.name + "

"); if (promoted.size() > 0) { printResults(promoted, pw, true, false); } else { pw.println("

(no API promoted)

"); } pw.println(); pw.println("
"); pw.println("

Added in " + newData.name + "

"); if (added.size() > 0) { printResults(added, pw, true, false); } else { pw.println("

(no API added)

"); } pw.println("
"); pw.println("

" + info + "
" + copyright + "

"); pw.println(""); pw.println(""); } else { pw.println(title); pw.println(); pw.println(); pw.println("=== Removed from " + oldData.name + " ==="); if (removed.size() > 0) { printResults(removed, pw, false, false); } else { pw.println("(no API removed)"); } pw.println(); pw.println(); if (internal) { pw.println("=== Withdrawn, Deprecated, or Obsoleted in " + newData.name + " ==="); } else { pw.println("=== Deprecated or Obsoleted in " + newData.name + " ==="); } if (obsoleted.size() > 0) { printResults(obsoleted, pw, false, false); } else { pw.println("(no API obsoleted)"); } pw.println(); pw.println(); pw.println("=== Changed in " + newData.name + " (old, new) ==="); if (changed.size() > 0) { printResults(changed, pw, false, true); } else { pw.println("(no API changed)"); } pw.println(); pw.println(); pw.println("=== Promoted to stable in " + newData.name + " ==="); if (promoted.size() > 0) { printResults(promoted, pw, false, false); } else { pw.println("(no API promoted)"); } pw.println(); pw.println(); pw.println("=== Added in " + newData.name + " ==="); if (added.size() > 0) { printResults(added, pw, false, false); } else { pw.println("(no API added)"); } pw.println(); pw.println("================"); pw.println(info); pw.println(copyright); } pw.close(); return false; } private static void printResults(Collection c, PrintWriter pw, boolean html, boolean isChangedAPIs) { Iterator iter = c.iterator(); String pack = null; String clas = null; while (iter.hasNext()) { APIInfo info = iter.next(); String packageName = info.getPackageName(); if (!packageName.equals(pack)) { if (html) { if (clas != null) { pw.println(""); } if (pack != null) { pw.println(""); } pw.println(); pw.println("

Package " + packageName + "

"); pw.print("
    "); } else { if (pack != null) { pw.println(); } pw.println(); pw.println("Package " + packageName + ":"); } pw.println(); pack = packageName; clas = null; } if (!info.isClass()) { String className = info.getClassName(); if (!className.equals(clas)) { if (html) { if (clas != null) { pw.println("
"); } pw.println(className); pw.println("
    "); } else { pw.println(className); } clas = className; } } if (html) { pw.print("
  • "); info.print(pw, isChangedAPIs, html); pw.println("
  • "); } else { info.println(pw, isChangedAPIs, html); } } if (html) { if (clas != null) { pw.println("
"); } if (pack != null) { pw.println(""); } } pw.println(); } private static TreeSet stripAndResort(TreeSet t) { stripClassInfo(t); TreeSet r = new TreeSet(APIInfo.classFirstComparator()); r.addAll(t); return r; } private static void stripClassInfo(Collection c) { // c is sorted with class info first Iterator iter = c.iterator(); String cname = null; while (iter.hasNext()) { APIInfo info = iter.next(); String className = info.getClassName(); if (cname != null) { if (cname.equals(className)) { iter.remove(); continue; } cname = null; } if (info.isClass()) { cname = info.getName(); } } } }