/** ******************************************************************************* * Copyright (C) 2004-2010, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ /** * Generate a list of ICU's public APIs, sorted by qualified name and signature * public APIs are all non-internal, non-package apis in com.ibm.icu.[lang|math|text|util]. * For each API, list * - public, package, protected, or private (PB PK PT PR) * - static or non-static (STK NST) * - final or non-final (FN NF) * - synchronized or non-synchronized (SYN NSY) * - stable, draft, deprecated, obsolete (ST DR DP OB) * - abstract or non-abstract (AB NA) * - constructor, member, field (C M F) * * Requires JDK 1.4.2 or later * * Sample invocation: * c:/j2sdk1.4.2/bin/javadoc * -classpath c:/jd2sk1.4.2/lib/tools.jar * -doclet com.ibm.icu.dev.tool.docs.CheckAPI * -docletpath c:/doug/cvsproj/icu4j/src * -sourcepath c:/eclipse2.1/workspace2/icu4j/src * -compare c:/doug/cvsproj/icu4j/src/com/ibm/icu/dev/tool/docs/api2_6_1.txt * -output foo * com.ibm.icu.text * * todo: separate generation of data files (which requires taglet) from * comparison and report generation (which does not require it) * todo: provide command-line control of filters of which subclasses/packages to process * todo: record full inheritance heirarchy, not just immediate inheritance * todo: allow for aliasing comparisons (force (pkg.)*class to be treated as though it * were in a different pkg/class heirarchy (facilitates comparison of icu4j and java) */ package com.ibm.icu.dev.tool.docs; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Iterator; import java.util.TreeSet; import com.sun.javadoc.ClassDoc; import com.sun.javadoc.ConstructorDoc; import com.sun.javadoc.Doc; import com.sun.javadoc.ExecutableMemberDoc; import com.sun.javadoc.FieldDoc; import com.sun.javadoc.MethodDoc; import com.sun.javadoc.ProgramElementDoc; import com.sun.javadoc.RootDoc; import com.sun.javadoc.Tag; public class CheckAPI { RootDoc root; String compare; // file String compareName; TreeSet compareSet; TreeSet results; boolean html; String srcName = "Current"; // default source name String output; private static final int DATA_FILE_VERSION = 1; private static final char SEP = ';'; private static final int STA = 0, STA_DRAFT = 0, STA_STABLE = 1, STA_DEPRECATED = 2, STA_OBSOLETE = 3; private static final int VIS = 1, VIS_PACKAGE = 0, VIS_PUBLIC= 1, VIS_PROTECTED = 2, VIS_PRIVATE = 3; private static final int STK = 2, STK_STATIC = 1; private static final int FIN = 3, FIN_FINAL = 1; private static final int SYN = 4, SYN_SYNCHRONIZED = 1; private static final int ABS = 5, ABS_ABSTRACT = 1; private static final int CAT = 6, CAT_CLASS = 0, CAT_FIELD = 1, CAT_CONSTRUCTOR = 2, CAT_METHOD = 3; private static final int PAK = 7; private static final int CLS = 8; private static final int NAM = 9; private static final int SIG = 10; private static final int EXC = 11; private static final int NUM_TYPES = 11; static abstract class APIInfo { public abstract int getVal(int typ); public abstract String get(int typ, boolean brief); public abstract void write(BufferedWriter w, boolean brief, boolean html, boolean detail); } final static class Info extends APIInfo { private int info; private String pack; // package private String cls; // enclosing class private String name; // name private String sig; // signature, class: inheritance, method: signature, field: type, const: signature private String exc; // throws public int getVal(int typ) { validateType(typ); return (info >> (typ*2)) & 0x3; } public String get(int typ, boolean brief) { validateType(typ); String[] vals = brief ? shortNames[typ] : names[typ]; if (vals == null) { switch (typ) { case PAK: return pack; case CLS: return cls; case NAM: return name; case SIG: return sig; case EXC: return exc; } } int val = (info >> (typ*2)) & 0x3; return vals[val]; } private void setType(int typ, int val) { validateType(typ); info &= ~(0x3 << (typ*2)); info |= (val&0x3) << (typ * 2); } private void setType(int typ, String val) { validateType(typ); String[] vals = shortNames[typ]; if (vals == null) { switch (typ) { case PAK: pack = val; break; case CLS: cls = val; break; case NAM: name = val; break; case SIG: sig = val; break; case EXC: exc = val; break; } return; } for (int i = 0; i < vals.length; ++i) { if (val.equalsIgnoreCase(vals[i])) { info &= ~(0x3 << (typ*2)); info |= i << (typ*2); return; } } throw new IllegalArgumentException("unrecognized value '" + val + "' for type '" + typeNames[typ] + "'"); } public void write(BufferedWriter w, boolean brief, boolean html, boolean detail) { try { if (brief) { for (int i = 0; i < NUM_TYPES; ++i) { String s = get(i, true); if (s != null) { w.write(s); } w.write(SEP); } } else { // remove all occurrences of icu packages from the param string // fortunately, all the packages have 4 chars (lang, math, text, util). String xsig = sig; if (!detail) { final String ICUPACK = "com.ibm.icu."; StringBuffer buf = new StringBuffer(); for (int i = 0; i < sig.length();) { int n = sig.indexOf(ICUPACK, i); if (n == -1) { buf.append(sig.substring(i)); break; } buf.append(sig.substring(i, n)); i = n + ICUPACK.length() + 5; // trailing 'xxxx.' } xsig = buf.toString(); } // construct signature for (int i = STA; i < CAT; ++i) { // include status String s = get(i, false); if (s != null && s.length() > 0) { if (i == STA) { w.write('('); w.write(s); w.write(')'); } else { w.write(s); } w.write(' '); } } int val = getVal(CAT); switch (val) { case CAT_CLASS: if (sig.indexOf("extends") == -1) { w.write("interface "); } else { w.write("class "); } if (cls.length() > 0) { w.write(cls); w.write('.'); } w.write(name); if (detail) { w.write(' '); w.write(sig); } break; case CAT_FIELD: w.write(xsig); w.write(' '); w.write(name); break; case CAT_METHOD: case CAT_CONSTRUCTOR: int n = xsig.indexOf('('); if (n > 0) { w.write(xsig.substring(0, n)); w.write(' '); } else { n = 0; } w.write(name); w.write(xsig.substring(n)); break; } } w.newLine(); } catch (IOException e) { RuntimeException re = new RuntimeException("IO Error"); re.initCause(e); throw re; } } public boolean read(BufferedReader r) { int i = 0; try { for (; i < NUM_TYPES; ++i) { setType(i, readToken(r)); } r.readLine(); // swallow line end sequence } catch (IOException e) { if (i == 0) { // assume if first read returns error, we have reached end of input return false; } RuntimeException re = new RuntimeException("IO Error"); re.initCause(e); throw re; } return true; } public boolean read(ProgramElementDoc doc) { // Doc. name // Doc. isField, isMethod, isConstructor, isClass, isInterface // ProgramElementDoc. containingClass, containingPackage // ProgramElementDoc. isPublic, isProtected, isPrivate, isPackagePrivate // ProgramElementDoc. isStatic, isFinal // MemberDoc.isSynthetic // ExecutableMemberDoc isSynchronized, signature // Type.toString() // e.g. "String[][]" // ClassDoc.isAbstract, superClass, interfaces, fields, methods, constructors, innerClasses // FieldDoc type // ConstructorDoc qualifiedName // MethodDoc isAbstract, returnType // status setType(STA, tagStatus(doc)); // visibility if (doc.isPublic()) { setType(VIS, VIS_PUBLIC); } else if (doc.isProtected()) { setType(VIS, VIS_PROTECTED); } else if (doc.isPrivate()) { setType(VIS, VIS_PRIVATE); } else { // default is package } // static if (doc.isStatic()) { setType(STK, STK_STATIC); } else { // default is non-static } // final if (doc.isFinal()) { setType(FIN, FIN_FINAL); } else { // default is non-final } // type if (doc.isField()) { setType(CAT, CAT_FIELD); } else if (doc.isMethod()) { setType(CAT, CAT_METHOD); } else if (doc.isConstructor()) { setType(CAT, CAT_CONSTRUCTOR); } else if (doc.isClass() || doc.isInterface()) { setType(CAT, CAT_CLASS); } setType(PAK, doc.containingPackage().name()); setType(CLS, (doc.isClass() || doc.isInterface() || (doc.containingClass() == null)) ? "" : doc.containingClass().name()); setType(NAM, doc.name()); if (doc instanceof FieldDoc) { FieldDoc fdoc = (FieldDoc)doc; setType(SIG, fdoc.type().toString()); } else if (doc instanceof ClassDoc) { ClassDoc cdoc = (ClassDoc)doc; if (cdoc.isClass() && cdoc.isAbstract()) { // interfaces are abstract by default, don't mark them as abstract setType(ABS, ABS_ABSTRACT); } StringBuffer buf = new StringBuffer(); if (cdoc.isClass()) { buf.append("extends "); buf.append(cdoc.superclass().qualifiedName()); } ClassDoc[] imp = cdoc.interfaces(); if (imp != null && imp.length > 0) { if (buf.length() > 0) { buf.append(" "); } buf.append("implements"); for (int i = 0; i < imp.length; ++i) { if (i != 0) { buf.append(","); } buf.append(" "); buf.append(imp[i].qualifiedName()); } } setType(SIG, buf.toString()); } else { ExecutableMemberDoc emdoc = (ExecutableMemberDoc)doc; if (emdoc.isSynchronized()) { setType(SYN, SYN_SYNCHRONIZED); } if (doc instanceof MethodDoc) { MethodDoc mdoc = (MethodDoc)doc; if (mdoc.isAbstract()) { setType(ABS, ABS_ABSTRACT); } setType(SIG, mdoc.returnType().toString() + emdoc.signature()); } else { // constructor setType(SIG, emdoc.signature()); } } return true; } public static Comparator defaultComparator() { final Comparator c = new Comparator() { public int compare(Object lhs, Object rhs) { Info lhi = (Info)lhs; Info rhi = (Info)rhs; int result = lhi.pack.compareTo(rhi.pack); if (result == 0) { result = (lhi.getVal(CAT) == CAT_CLASS ? lhi.name : lhi.cls) .compareTo(rhi.getVal(CAT) == CAT_CLASS ? rhi.name : rhi.cls); if (result == 0) { result = lhi.getVal(CAT)- rhi.getVal(CAT); if (result == 0) { result = lhi.name.compareTo(rhi.name); if (result == 0) { result = lhi.sig.compareTo(rhi.sig); } } } } return result; } }; return c; } public static Comparator changedComparator() { final Comparator c = new Comparator() { public int compare(Object lhs, Object rhs) { Info lhi = (Info)lhs; Info rhi = (Info)rhs; int result = lhi.pack.compareTo(rhi.pack); if (result == 0) { result = (lhi.getVal(CAT) == CAT_CLASS ? lhi.name : lhi.cls) .compareTo(rhi.getVal(CAT) == CAT_CLASS ? rhi.name : rhi.cls); if (result == 0) { result = lhi.getVal(CAT)- rhi.getVal(CAT); if (result == 0) { result = lhi.name.compareTo(rhi.name); if (result == 0 && lhi.getVal(CAT) != CAT_CLASS) { result = lhi.sig.compareTo(rhi.sig); } } } } return result; } }; return c; } public static Comparator classFirstComparator() { final Comparator c = new Comparator() { public int compare(Object lhs, Object rhs) { Info lhi = (Info)lhs; Info rhi = (Info)rhs; int result = lhi.pack.compareTo(rhi.pack); if (result == 0) { boolean lcls = lhi.getVal(CAT) == CAT_CLASS; boolean rcls = rhi.getVal(CAT) == CAT_CLASS; result = lcls == rcls ? 0 : (lcls ? -1 : 1); if (result == 0) { result = (lcls ? lhi.name : lhi.cls).compareTo(rcls ? rhi.name : rhi.cls); if (result == 0) { result = lhi.getVal(CAT)- rhi.getVal(CAT); if (result == 0) { result = lhi.name.compareTo(rhi.name); if (result == 0 && !lcls) { result = lhi.sig.compareTo(rhi.sig); } } } } } return result; } }; return c; } private static final String[] typeNames = { "status", "visibility", "static", "final", "synchronized", "abstract", "category", "package", "class", "name", "signature" }; private static final String[][] names = { { "draft ", "stable ", "deprecated", "obsolete " }, { "package", "public", "protected", "private" }, { "", "static" }, { "", "final" }, { "", "synchronized" }, { "", "abstract" }, { "class", "field", "constructor", "method" }, null, null, null, null, null }; private static final String[][] shortNames = { { "DR", "ST", "DP", "OB" }, { "PK", "PB", "PT", "PR" }, { "NS", "ST" }, { "NF", "FN" }, { "NS", "SY" }, { "NA", "AB" }, { "L", "F", "C", "M" }, null, null, null, null, null }; private static void validateType(int typ) { if (typ < 0 || typ > NUM_TYPES) { throw new IllegalArgumentException("bad type index: " + typ); } } public String toString() { return get(NAM, true); } } static final class DeltaInfo extends APIInfo { private Info a; private Info b; DeltaInfo(Info a, Info b) { this.a = a; this.b = b; } public int getVal(int typ) { return a.getVal(typ); } public String get(int typ, boolean brief) { return a.get(typ, brief); } public void write(BufferedWriter w, boolean brief, boolean html, boolean detail) { a.write(w, brief, html, detail); try { if (html) { w.write("
"); } w.newLine(); } catch (Exception e) { } b.write(w, brief, html, detail); } public String toString() { return a.get(NAM, true); } } public static int optionLength(String option) { if (option.equals("-html")) { return 1; } else if (option.equals("-name")) { return 2; } else if (option.equals("-output")) { return 2; } else if (option.equals("-compare")) { return 2; } return 0; } public static boolean start(RootDoc root) { return new CheckAPI(root).run(); } CheckAPI(RootDoc root) { this.root = root; // this.compare = "c:/doug/cvsproj/icu4j/src/com/ibm/icu/dev/tool/docs/api2_8.txt"; String[][] options = root.options(); for (int i = 0; i < options.length; ++i) { String opt = options[i][0]; if (opt.equals("-html")) { this.html = true; } else if (opt.equals("-name")) { this.srcName = options[i][1]; } else if (opt.equals("-output")) { this.output = options[i][1]; } else if (opt.equals("-compare")) { this.compare = options[i][1]; } } if (compare != null) { try { // URL url = new URL(compare); File f = new File(compare); InputStream is = new FileInputStream(f); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); // read header line /*int version = */Integer.parseInt(readToken(br)); // check version if we change it later, probably can just rebuild though this.compareName = readToken(br); br.readLine(); // read data this.compareSet = new TreeSet(Info.defaultComparator()); for (Info info = new Info(); info.read(br); info = new Info()) { compareSet.add(info); } } catch (Exception e) { RuntimeException re = new RuntimeException("error reading " + compare); re.initCause(e); throw re; } } results = new TreeSet(Info.defaultComparator()); } private boolean run() { doDocs(root.classes()); OutputStream os = System.out; if (output != null) { try { os = new FileOutputStream(output); } catch (FileNotFoundException e) { RuntimeException re = new RuntimeException(e.getMessage()); re.initCause(e); throw re; } } BufferedWriter bw = null; try { OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8"); bw = new BufferedWriter(osw); if (compareSet == null) { // writing data file bw.write(String.valueOf(DATA_FILE_VERSION) + SEP); // header version bw.write(srcName + SEP); // source name bw.newLine(); writeResults(results, bw, true, false, false); } else { // writing comparison info TreeSet removed = (TreeSet)compareSet.clone(); removed.removeAll(results); TreeSet added = (TreeSet)results.clone(); added.removeAll(compareSet); Iterator ai = added.iterator(); Iterator ri = removed.iterator(); ArrayList changed = new ArrayList(); Comparator c = Info.changedComparator(); Info a = null, r = null; while (ai.hasNext() && ri.hasNext()) { if (a == null) a = (Info)ai.next(); if (r == null) r = (Info)ri.next(); 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; ai.remove(); r = null; ri.remove(); } } added = stripAndResort(added); removed = stripAndResort(removed); if (html) { String title = "ICU4J API Comparison: " + srcName + " with " + compareName; bw.write(""); bw.newLine(); bw.write(""); bw.newLine(); bw.write(""); bw.newLine(); bw.write(""); bw.newLine(); bw.write(""); bw.write(title); bw.write(""); bw.newLine(); bw.write(""); bw.newLine(); bw.write("

"); bw.write(title); bw.write("

"); bw.newLine(); bw.write("
"); bw.newLine(); bw.write("

"); bw.write("Removed from " + compareName); bw.write("

"); bw.newLine(); if (removed.size() > 0) { writeResults(removed, bw, false, true, false); } else { bw.write("

(no API removed)

"); } bw.newLine(); bw.write("
"); bw.newLine(); bw.write("

"); bw.write("Changed in " + srcName); bw.write("

"); bw.newLine(); if (changed.size() > 0) { writeResults(changed, bw, false, true, true); } else { bw.write("

(no API changed)

"); } bw.newLine(); bw.write("
"); bw.newLine(); bw.write("

"); bw.write("Added in " + srcName); bw.write("

"); bw.newLine(); if (added.size() > 0) { writeResults(added, bw, false, true, false); } else { bw.write("

(no API added)

"); } bw.write("
"); bw.newLine(); bw.write("

Contents generated by CheckAPI tool.
Copyright (C) 2004, International Business Machines Corporation, All Rights Reserved.

"); bw.newLine(); bw.write(""); bw.newLine(); bw.write(""); bw.newLine(); } else { bw.write("Comparing " + srcName + " with " + compareName); bw.newLine(); bw.newLine(); bw.newLine(); bw.write("=== Removed from " + compareName + " ==="); bw.newLine(); if (removed.size() > 0) { writeResults(removed, bw, false, false, false); } else { bw.write("(no API removed)"); } bw.newLine(); bw.newLine(); bw.write("=== Changed in " + srcName + " ==="); bw.newLine(); if (changed.size() > 0) { writeResults(changed, bw, false, false, true); } else { bw.write("(no API changed)"); } bw.newLine(); bw.newLine(); bw.write("=== Added in " + srcName + " ==="); bw.newLine(); if (added.size() > 0) { writeResults(added, bw, false, false, false); } else { bw.write("(no API added)"); } bw.newLine(); } } bw.close(); } catch (IOException e) { try { bw.close(); } catch (IOException e2) {} RuntimeException re = new RuntimeException("write error: " + e.getMessage()); re.initCause(e); throw re; } return false; } private void doDocs(ProgramElementDoc[] docs) { if (docs != null && docs.length > 0) { for (int i = 0; i < docs.length; ++i) { doDoc(docs[i]); } } } private void doDoc(ProgramElementDoc doc) { if (ignore(doc)) return; if (doc.isClass() || doc.isInterface()) { ClassDoc cdoc = (ClassDoc)doc; doDocs(cdoc.fields()); doDocs(cdoc.constructors()); doDocs(cdoc.methods()); doDocs(cdoc.innerClasses()); } Info info = new Info(); if (info.read(doc)) { results.add(info); } } private boolean ignore(ProgramElementDoc doc) { if (doc == null) return true; if (doc.isPrivate() || doc.isPackagePrivate()) return true; if (doc instanceof ConstructorDoc && ((ConstructorDoc)doc).isSynthetic()) return true; if (doc.qualifiedName().indexOf(".misc") != -1) return true; Tag[] tags = doc.tags(); for (int i = 0; i < tags.length; ++i) { if (tagKindIndex(tags[i].kind()) == INTERNAL) return true; } return false; } private static void writeResults(Collection c, BufferedWriter w, boolean brief, boolean html, boolean detail) { Iterator iter = c.iterator(); String pack = null; String clas = null; while (iter.hasNext()) { APIInfo info = (APIInfo)iter.next(); if (brief) { info.write(w, brief, false, detail); } else { try { String p = info.get(PAK, true); if (!p.equals(pack)) { w.newLine(); if (html) { if (clas != null) { w.write(""); w.newLine(); } if (pack != null) { w.write(""); w.newLine(); } w.write("

Package "); w.write(p); w.write("

"); w.newLine(); w.write(""); } w.write("
  • "); w.write(name); w.newLine(); w.write(""); w.newLine(); } if (pack != null) { w.write(""); w.newLine(); } } catch (IOException e) { } } } private static String readToken(BufferedReader r) throws IOException { char[] buf = new char[256]; int i = 0; for (; i < buf.length; ++i) { int c = r.read(); if (c == -1) { throw new IOException("unexpected EOF"); } else if (c == SEP) { break; } buf[i] = (char)c; } if (i == buf.length) { throw new IOException("unterminated token" + new String(buf)); } return new String(buf, 0, i); } private static TreeSet stripAndResort(TreeSet t) { stripClassInfo(t); TreeSet r = new TreeSet(Info.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()) { Info info = (Info)iter.next(); String cls = info.get(CLS, true); if (cname != null) { if (cname.equals(cls)) { iter.remove(); continue; } cname = null; } if (info.getVal(CAT) == CAT_CLASS) { cname = info.get(NAM, true); } } } private static int tagStatus(final Doc doc) { class Result { int res = -1; void set(int val) { if (res != -1) throw new RuntimeException("bad doc: " + doc); res = val; } int get() { if (res == -1) { System.err.println("warning: no tag for " + doc); return 0; } return res; } } Tag[] tags = doc.tags(); Result result = new Result(); for (int i = 0; i < tags.length; ++i) { Tag tag = tags[i]; String kind = tag.kind(); int ix = tagKindIndex(kind); switch (ix) { case INTERNAL: result.set(-2); break; case DRAFT: result.set(STA_DRAFT); break; case STABLE: result.set(STA_STABLE); break; case DEPRECATED: result.set(STA_DEPRECATED); break; case OBSOLETE: result.set(STA_OBSOLETE); break; case SINCE: case EXCEPTION: case VERSION: case UNKNOWN: case AUTHOR: case SEE: case PARAM: case RETURN: case THROWS: case SERIAL: break; default: throw new RuntimeException("unknown index " + ix + " for tag: " + kind); } } return result.get(); } private static final int UNKNOWN = -1; private static final int INTERNAL = 0; private static final int DRAFT = 1; private static final int STABLE = 2; private static final int SINCE = 3; private static final int DEPRECATED = 4; private static final int AUTHOR = 5; private static final int SEE = 6; private static final int VERSION = 7; private static final int PARAM = 8; private static final int RETURN = 9; private static final int THROWS = 10; private static final int OBSOLETE = 11; private static final int EXCEPTION = 12; private static final int SERIAL = 13; private static int tagKindIndex(String kind) { final String[] tagKinds = { "@internal", "@draft", "@stable", "@since", "@deprecated", "@author", "@see", "@version", "@param", "@return", "@throws", "@obsolete", "@exception", "@serial" }; for (int i = 0; i < tagKinds.length; ++i) { if (kind.equals(tagKinds[i])) { return i; } } return UNKNOWN; } }