blob: 2e10ba6071d930d6ff40596dc8d5f64c3ae1ecc3 [file] [log] [blame]
/*
*******************************************************************************
* Copyright (C) 2005-2006, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*
*/
package com.ibm.icu.dev.tool.docs;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
/**
* 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 <init> 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 ? "<init>" : 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);
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);
}