/* ******************************************************************************* * Copyright (C) 2005-2010, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* * */ package com.ibm.icu.dev.tool.docs; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; /** * Compare ICU4J and JDK APIS. * * TODO: compare protected APIs. Reflection on Class allows you * to either get all inherited methods with public access, or get methods * on the particular class with any access, but no way to get all * inherited methods with any access. Go figure. */ public class ICUJDKCompare { static final boolean DEBUG = false; // set up defaults private static final String kSrcPrefix = "java."; private static final String kTrgPrefix = "com.ibm.icu."; private static final String[] kPairInfo = { "lang.Character/UCharacter", "lang.Character$UnicodeBlock/UCharacter$UnicodeBlock", "text.BreakIterator", "text.Collator", "text.DateFormat", "text.DateFormatSymbols", "text.DecimalFormat", "text.DecimalFormatSymbols", "text.Format/UFormat", "text.MessageFormat", "text.NumberFormat", "text.SimpleDateFormat", "util.Calendar", "util.Currency", "util.GregorianCalendar", "util.SimpleTimeZone", "util.TimeZone", "util.Locale/ULocale", "util.ResourceBundle/UResourceBundle", }; private static final String[] kIgnore = new String[] { "lang.Character charValue compareTo MAX_VALUE MIN_VALUE TYPE", "lang.Character$UnicodeBlock SURROGATES_AREA", "util.Calendar FIELD_COUNT", "util.GregorianCalendar FIELD_COUNT", "util.SimpleTimeZone STANDARD_TIME UTC_TIME WALL_TIME", }; private PrintWriter pw; private String srcPrefix; private String trgPrefix; private Class[] classPairs; private String[] namePairs; private String[] ignore; private boolean swap; //private boolean signature; // call System.exit with non-zero if there were some missing APIs public static void main(String[] args) { System.exit(doMain(args)); } // return non-zero if there were some missing APIs public static int doMain(String[] args) { ICUJDKCompare p = new ICUJDKCompare(); p.setOutputWriter(new PrintWriter(System.out)); p.setup(args); return p.process(); } // setters public ICUJDKCompare setOutputWriter(PrintWriter pw) { this.pw = pw; return this; } public ICUJDKCompare setSrcPrefix(String srcPrefix) { this.srcPrefix = srcPrefix; return this; } public ICUJDKCompare setTrgPrefix(String trgPrefix) { this.trgPrefix = trgPrefix; return this; } public ICUJDKCompare setClassPairs(Class[] classPairs) { this.classPairs = classPairs; return this; } public ICUJDKCompare setNamePairs(String[] namePairs) { this.namePairs = namePairs; return this; } public ICUJDKCompare setIgnore(String[] ignore) { this.ignore = ignore; return this; } public ICUJDKCompare setSwap(boolean swap) { this.swap = swap; return this; } public ICUJDKCompare setup(String[] args) { String namelist = null; String ignorelist = null; for (int i = 0; i < args.length; ++i) { String arg = args[i]; if (arg.equals("-swap")) { swap = true; } else if (arg.equals("-srcPrefix:")) { srcPrefix = args[++i]; if (!srcPrefix.endsWith(".")) { srcPrefix += '.'; } } else if (arg.equals("-trgPrefix:")) { trgPrefix = args[++i]; if (!trgPrefix.endsWith(".")) { trgPrefix += '.'; } } else if (arg.equals("-names:")) { namelist = args[++i]; } else if (arg.equals("-ignore:")) { ignorelist = args[++i]; } else { System.err.println("unrecognized argument: " + arg); throw new IllegalStateException(); } } if (ignorelist != null) { if (ignorelist.charAt(0) == '@') { // a file containing ignoreinfo try { ArrayList nl = new ArrayList(); File f = new File(namelist.substring(1)); FileInputStream fis = new FileInputStream(f); InputStreamReader isr = new InputStreamReader(fis); BufferedReader br = new BufferedReader(isr); String line = null; while (null != (line = br.readLine())) { nl.add(line); } ignore = (String[])nl.toArray(new String[nl.size()]); } catch (Exception e) { System.err.println(e); throw new IllegalStateException(); } } else { // a list of ignoreinfo separated by semicolons ignore = ignorelist.split("\\s*;\\s*"); } } if (namelist != null) { String[] names = null; if (namelist.charAt(0) == '@') { // a file try { ArrayList nl = new ArrayList(); File f = new File(namelist.substring(1)); FileInputStream fis = new FileInputStream(f); InputStreamReader isr = new InputStreamReader(fis); BufferedReader br = new BufferedReader(isr); String line = null; while (null != (line = br.readLine())) { nl.add(line); } names = (String[])nl.toArray(new String[nl.size()]); } catch (Exception e) { System.err.println(e); throw new IllegalStateException(); } } else { // a list of names separated by semicolons names = namelist.split("\\s*;\\s*"); } processPairInfo(names); } pw.flush(); return this; } private void processPairInfo(String[] names) { ArrayList cl = new ArrayList(); ArrayList nl = new ArrayList(); for (int i = 0; i < names.length; ++i) { String name = names[i]; String srcName = srcPrefix; String trgName = trgPrefix; int n = name.indexOf('/'); if (n == -1) { srcName += name; trgName += name; } else { String srcSuffix = name.substring(0, n).trim(); String trgSuffix = name.substring(n+1).trim(); int jx = srcSuffix.length()+1; int ix = trgSuffix.length()+1; while (ix != -1) { jx = srcSuffix.lastIndexOf('.', jx-1); ix = trgSuffix.lastIndexOf('.', ix-1); } srcName += srcSuffix; trgName += srcSuffix.substring(0, jx+1) + trgSuffix; } try { Class jc = Class.forName(srcName); Class ic = Class.forName(trgName); cl.add(ic); cl.add(jc); nl.add(ic.getName()); nl.add(jc.getName()); } catch (Exception e) { if (DEBUG) System.err.println("can't load class: " + e.getMessage()); } } classPairs = (Class[])cl.toArray(new Class[cl.size()]); namePairs = (String[])nl.toArray(new String[nl.size()]); } private void println(String s) { if (pw != null) pw.println(s); } private void flush() { if (pw != null) pw.flush(); } public int process() { // set defaults if (srcPrefix == null) { srcPrefix = kSrcPrefix; } if (trgPrefix == null) { trgPrefix = kTrgPrefix; } if (classPairs == null) { processPairInfo(kPairInfo); } if (ignore == null) { ignore = kIgnore; } println("ICU and Java API Comparison"); String ICU_VERSION = "unknown"; try { Class cls = Class.forName("com.ibm.icu.util.VersionInfo"); Field fld = cls.getField("ICU_VERSION"); ICU_VERSION = fld.get(null).toString(); } catch (Exception e) { if (DEBUG) System.err.println("can't get VersionInfo: " + e.getMessage()); } println("ICU Version " + ICU_VERSION); println("JDK Version " + System.getProperty("java.version")); int errorCount = 0; for (int i = 0; i < classPairs.length; i += 2) { try { if (swap) { errorCount += compare(classPairs[i+1], classPairs[i]); } else { errorCount += compare(classPairs[i], classPairs[i+1]); } } catch (Exception e) { System.err.println("exception: " + e); System.err.println("between " + namePairs[i] + " and " + namePairs[i+1]); e.printStackTrace(); errorCount += 1; } } return errorCount; } static class MorC { private Method mref; private Constructor cref; MorC(Method m) { mref = m; } MorC(Constructor c) { cref = c; } int getModifiers() { return mref == null ? cref.getModifiers() : mref.getModifiers(); } Class getReturnType() { return mref == null ? void.class : mref.getReturnType(); } Class[] getParameterTypes() { return mref == null ? cref.getParameterTypes() : mref.getParameterTypes(); } String getName() { return mref == null ? "" : mref.getName(); } String getSignature() { return mref == null ? cref.toString() : mref.toString(); } } private int compare(Class class1, Class class2) throws Exception { String n1 = class1.getName(); String n2 = class2.getName(); println("\ncompare " + n1 + " <> " + n2); MorC[] conss1 = getMorCArray(class1.getConstructors()); MorC[] conss2 = getMorCArray(class2.getConstructors()); Map cmap1 = getMethodMap(conss1); Map cmap2 = getMethodMap(conss2); MorC[] meths1 = getMorCArray(class1.getMethods()); MorC[] meths2 = getMorCArray(class2.getMethods()); Map map1 = getMethodMap(meths1); Map map2 = getMethodMap(meths2); Field[] fields1 = class1.getFields(); Field[] fields2 = class2.getFields(); Set set1 = getFieldSet(fields1); Set set2 = getFieldSet(fields2); if (n1.indexOf("DecimalFormatSymbols") != -1) { pw.format("fields in %s: %s%n", n1, set1); pw.format("fields in %s: %s%n", n2, set2); } Map diffConss = diffMethodMaps(cmap2, cmap1); Map diffMeths = diffMethodMaps(map2, map1); Set diffFields = diffFieldSets(set2, set1); diffConss = removeIgnored(n2, diffConss); diffMeths = removeIgnored(n2, diffMeths); diffFields = removeIgnored(n2, diffFields); int result = diffConss.size() + diffMeths.size() + diffFields.size(); if (result > 0 && pw != null) { pw.println("Public API in " + n2 + " but not in " + n1); if (diffConss.size() > 0) { pw.println("CONSTRUCTORS"); dumpMethodMap(diffConss, pw); } if (diffMeths.size() > 0) { pw.println("METHODS"); dumpMethodMap(diffMeths, pw); } if (diffFields.size() > 0) { pw.println("FIELDS"); dumpFieldSet(diffFields, pw); } } flush(); return result; } final class MethodRecord { MorC[] overrides; MethodRecord(MorC m) { overrides = new MorC[] { m }; } MethodRecord(MorC[] ms) { overrides = ms; } MethodRecord copy() { return new MethodRecord((MorC[])overrides.clone()); } int count() { for (int i = 0; i < overrides.length; ++i) { if (overrides[i] == null) { return i; } } return overrides.length; } void add(MorC m) { MorC[] temp = new MorC[overrides.length + 1]; for (int i = 0; i < overrides.length; ++i) { temp[i] = overrides[i]; } temp[overrides.length] = m; overrides = temp; } void remove(int index) { int i = index; while (overrides[i] != null && i < overrides.length-1) { overrides[i] = overrides[i+1]; ++i; } overrides[i] = null; } // if a call to a method can be handled by a call to t, remove the // method from our list, and return true boolean removeOverridden(MorC t) { boolean result = false; int i = 0; while (i < overrides.length) { MorC m = overrides[i]; if (m == null) { break; } if (handles(t, m)) { remove(i); result = true; } else { ++i; } } return result; } // remove all methods handled by any method of mr boolean removeOverridden(MethodRecord mr) { boolean result = false; for (int i = 0; i < mr.overrides.length; ++i) { MorC t = mr.overrides[i]; if (t == null) { // this shouldn't happen, as the target record should not have been modified throw new IllegalStateException(); } if (removeOverridden(t)) { result = true; } } return result; } void debugmsg(MorC t, MorC m, String msg) { StringBuffer buf = new StringBuffer(); buf.append(t.getName()); buf.append(" "); buf.append(msg); buf.append("\n "); toString(t, buf); buf.append("\n "); toString(m, buf); System.out.println(buf.toString()); } boolean handles(MorC t, MorC m) { // relevant modifiers must match if ((t.getModifiers() & MOD_MASK) != (m.getModifiers() & MOD_MASK)) { if (DEBUG) debugmsg(t, m, "modifier mismatch"); return false; } Class tr = pairClassEquivalent(t.getReturnType()); Class mr = pairClassEquivalent(m.getReturnType()); if (!assignableFrom(mr, tr)) { // t return type must be same or narrower than m if (DEBUG) debugmsg(t, m, "return value mismatch"); return false; } Class[] tts = t.getParameterTypes(); Class[] mts = m.getParameterTypes(); if (tts.length != mts.length) { if (DEBUG) debugmsg(t, m, "param count mismatch"); return false; } for (int i = 0; i < tts.length; ++i) { Class tc = pairClassEquivalent(tts[i]); Class mc = pairClassEquivalent(mts[i]); if (!assignableFrom(tc, mc)) { // m param must be same or narrower than t if (DEBUG) debugmsg(t, m, "parameter " + i + " mismatch, " + tts[i].getName() + " not assignable from " + mts[i].getName()); return false; } } return true; } public void toString(MorC m, StringBuffer buf) { int mod = m.getModifiers(); if (mod != 0) { buf.append(Modifier.toString(mod) + " "); } buf.append(nameOf(m.getReturnType())); buf.append(" "); buf.append(m.getName()); buf.append("("); Class[] ptypes = m.getParameterTypes(); for (int j = 0; j < ptypes.length; ++j) { if (j > 0) { buf.append(", "); } buf.append(nameOf(ptypes[j])); } buf.append(')'); } public String toString() { StringBuffer buf = new StringBuffer(); buf.append(overrides[0].getName()); for (int i = 0; i < overrides.length; ++i) { MorC m = overrides[i]; if (m == null) { break; } buf.append("\n "); toString(m, buf); } return buf.toString(); } } public static String nameOf(Class c) { if (c.isArray()) { return nameOf(c.getComponentType()) + "[]"; } String name = c.getName(); return name.substring(name.lastIndexOf('.') + 1); } static MorC[] getMorCArray(Constructor[] cons) { MorC[] result = new MorC[cons.length]; for (int i = 0 ; i < cons.length; ++i) { result[i] = new MorC(cons[i]); } return result; } static MorC[] getMorCArray(Method[] meths) { MorC[] result = new MorC[meths.length]; for (int i = 0 ; i < meths.length; ++i) { result[i] = new MorC(meths[i]); } return result; } private Map getMethodMap(MorC[] meths) { Map result = new TreeMap(); for (int i = 0; i < meths.length; ++i) { MorC m = meths[i]; String key = m.getName(); MethodRecord mr = (MethodRecord)result.get(key); if (mr == null) { mr = new MethodRecord(m); result.put(key, mr); } else { mr.add(m); } } return result; } private void dumpMethodMap(Map m, PrintWriter pw) { Iterator iter = m.entrySet().iterator(); while (iter.hasNext()) { dumpMethodRecord((MethodRecord)((Map.Entry)iter.next()).getValue()); } pw.flush(); } private void dumpMethodRecord(MethodRecord mr) { pw.println(mr.toString()); } static Map diffMethodMaps(Map m1, Map m2) { // get all the methods in m1 that aren't mentioned in m2 at all Map result = (Map)((TreeMap)m1).clone(); result.keySet().removeAll(m2.keySet()); return result; } private Map removeIgnored(String name, Map m1) { if (ignore == null) { return m1; } if (name.startsWith(srcPrefix)) { name = name.substring(srcPrefix.length()); } name += " "; // to avoid accidental prefix of nested class name // prune ignore list to relevant items ArrayList il = null; for (int i = 0; i < ignore.length; ++i) { String s = ignore[i]; if (s.startsWith(name)) { if (il == null) { il = new ArrayList(); } il.add(s); } } if (il == null) { return m1; } Map result = new TreeMap(((TreeMap)m1).comparator()); result.putAll(m1); Iterator iter = result.entrySet().iterator(); loop: while (iter.hasNext()) { Map.Entry e = (Map.Entry)iter.next(); String key = (String)e.getKey(); for (int i = 0; i < il.size(); ++i) { String ig = (String)il.get(i); if (ig.indexOf(" " + key) != 0) { iter.remove(); continue loop; } } } return result; } private Set removeIgnored(String name, Set s1) { if (ignore == null) { return s1; } if (name.startsWith(srcPrefix)) { name = name.substring(srcPrefix.length()); } name += " "; // to avoid accidental prefix of nested class name // prune ignore list to relevant items ArrayList il = null; for (int i = 0; i < ignore.length; ++i) { String s = ignore[i]; if (s.startsWith(name)) { if (il == null) { il = new ArrayList(); } il.add(s); } } if (il == null) { return s1; } Set result = (Set)((TreeSet)s1).clone(); Iterator iter = result.iterator(); loop: while (iter.hasNext()) { String key = (String)iter.next(); String fieldname = key.substring(0, key.indexOf(' ')); for (int i = 0; i < il.size(); ++i) { String ig = (String)il.get(i); if (ig.indexOf(" " + fieldname) != 0) { iter.remove(); continue loop; } } } return result; } static final boolean[][] assignmentMap = { // bool char byte short int long float double void { true, false, false, false, false, false, false, false, false }, // boolean { false, true, true, true, false, false, false, false, false }, // char { false, false, true, false, false, false, false, false, false }, // byte { false, false, true, true, false, false, false, false, false }, // short { false, true, true, true, true, false, false, false, false }, // int { false, true, true, true, true, true, false, false, false }, // long { false, true, true, true, true, false, true, false, false }, // float { false, true, true, true, true, false, true, true, false }, // double { false, false, false, false, false, false, false, false, true }, // void }; static final Class[] prims = { boolean.class, char.class, byte.class, short.class, int.class, long.class, float.class, double.class, void.class }; static int primIndex(Class cls) { for (int i = 0; i < prims.length; ++i) { if (cls == prims[i]) { return i; } } throw new IllegalStateException("could not find primitive class: " + cls); } static boolean assignableFrom(Class lhs, Class rhs) { if (lhs == rhs) { return true; } if (lhs.isPrimitive()) { if (!rhs.isPrimitive()) { return false; } int lhsx = primIndex(lhs); int rhsx = primIndex(rhs); return assignmentMap[lhsx][rhsx]; } return lhs.isAssignableFrom(rhs); } private String toString(Field f) { StringBuffer buf = new StringBuffer(f.getName()); int mod = f.getModifiers() & MOD_MASK; if (mod != 0) { buf.append(" " + Modifier.toString(mod)); } buf.append(" "); String n = pairEquivalent(f.getType().getName()); n = n.substring(n.lastIndexOf('.') + 1); buf.append(n); return buf.toString(); } private Set getFieldSet(Field[] fs) { Set set = new TreeSet(); for (int i = 0; i < fs.length; ++i) { set.add(toString(fs[i])); } return set; } static Set diffFieldSets(Set s1, Set s2) { Set result = (Set)((TreeSet)s1).clone(); result.removeAll(s2); return result; } private void dumpFieldSet(Set s, PrintWriter pw) { Iterator iter = s.iterator(); while (iter.hasNext()) { pw.println(iter.next()); } pw.flush(); } // given a target string, if it matches the first of one of our pairs, return the second // or vice-versa if swap is true private String pairEquivalent(String target) { for (int i = 0; i < namePairs.length; i += 2) { if (swap) { if (target.equals(namePairs[i+1])) { return namePairs[i]; } } else { if (target.equals(namePairs[i])) { return namePairs[i+1]; } } } return target; } private Class pairClassEquivalent(Class target) { for (int i = 0; i < classPairs.length; i += 2) { if (target.equals(classPairs[i])) { return classPairs[i+1]; } } return target; } static final int MOD_MASK = ~(Modifier.FINAL|Modifier.SYNCHRONIZED| Modifier.VOLATILE|Modifier.TRANSIENT|Modifier.NATIVE); }