001: /**
002: *******************************************************************************
003: * Copyright (C) 2004-2006, International Business Machines Corporation and *
004: * others. All Rights Reserved. *
005: *******************************************************************************
006: */
007:
008: /**
009: * Compare two API files (generated by GatherAPIData) and generate a report
010: * on the differences.
011: *
012: * Sample invocation:
013: * java -old: icu4j28.api.zip -new: icu4j30.api -html -out: icu4j_compare_28_30.html
014: *
015: * TODO:
016: * - make 'changed apis' smarter - detect method parameter or return type change
017: * for this, the sequential search through methods ordered by signature won't do.
018: * We need to gather all added and removed overloads for a method, and then
019: * compare all added against all removed in order to identify this kind of
020: * change.
021: */package com.ibm.icu.dev.tool.docs;
022:
023: import java.io.*;
024: import java.util.*;
025: import java.text.*;
026:
027: public class ReportAPI {
028: APIData oldData;
029: APIData newData;
030: boolean html;
031: String outputFile;
032:
033: TreeSet added;
034: TreeSet removed;
035: TreeSet promoted;
036: TreeSet obsoleted;
037: ArrayList changed;
038:
039: static final class DeltaInfo extends APIInfo {
040: APIInfo added;
041: APIInfo removed;
042:
043: DeltaInfo(APIInfo added, APIInfo removed) {
044: this .added = added;
045: this .removed = removed;
046: }
047:
048: public int getVal(int typ) {
049: return added.getVal(typ);
050: }
051:
052: public String get(int typ, boolean brief) {
053: return added.get(typ, brief);
054: }
055:
056: public void print(PrintWriter pw, boolean detail, boolean html) {
057: pw.print(" ");
058: removed.print(pw, detail, html);
059: if (html) {
060: pw.println("</br>");
061: } else {
062: pw.println();
063: pw.print("--> ");
064: }
065: added.print(pw, detail, html);
066: }
067: }
068:
069: public static void main(String[] args) {
070: String oldFile = null;
071: String newFile = null;
072: String outFile = null;
073: boolean html = false;
074: boolean internal = false;
075: for (int i = 0; i < args.length; ++i) {
076: String arg = args[i];
077: if (arg.equals("-old:")) {
078: oldFile = args[++i];
079: } else if (arg.equals("-new:")) {
080: newFile = args[++i];
081: } else if (arg.equals("-out:")) {
082: outFile = args[++i];
083: } else if (arg.equals("-html")) {
084: html = true;
085: } else if (arg.equals("-internal")) {
086: internal = true;
087: }
088: }
089:
090: new ReportAPI(oldFile, newFile, internal).writeReport(outFile,
091: html, internal);
092: }
093:
094: /*
095: while the both are methods and the class and method names are the same, collect
096: overloads. when you hit a new method or class, compare the overloads
097: looking for the same # of params and simple param changes. ideally
098: there are just a few.
099:
100: String oldA = null;
101: String oldR = null;
102: if (!a.isMethod()) {
103: remove and continue
104: }
105: String am = a.getClassName() + "." + a.getName();
106: String rm = r.getClassName() + "." + r.getName();
107: int comp = am.compare(rm);
108: if (comp == 0 && a.isMethod() && r.isMethod())
109:
110: */
111:
112: ReportAPI(String oldFile, String newFile, boolean internal) {
113: this (APIData.read(oldFile, internal), APIData.read(newFile,
114: internal));
115: }
116:
117: ReportAPI(APIData oldData, APIData newData) {
118: this .oldData = oldData;
119: this .newData = newData;
120:
121: removed = (TreeSet) oldData.set.clone();
122: removed.removeAll(newData.set);
123:
124: added = (TreeSet) newData.set.clone();
125: added.removeAll(oldData.set);
126:
127: changed = new ArrayList();
128: Iterator ai = added.iterator();
129: Iterator ri = removed.iterator();
130: Comparator c = APIInfo.changedComparator();
131:
132: ArrayList ams = new ArrayList();
133: ArrayList rms = new ArrayList();
134: PrintWriter outpw = new PrintWriter(System.out);
135:
136: APIInfo a = null, r = null;
137: while ((a != null || ai.hasNext())
138: && (r != null || ri.hasNext())) {
139: if (a == null)
140: a = (APIInfo) ai.next();
141: if (r == null)
142: r = (APIInfo) ri.next();
143:
144: String am = a.getClassName() + "." + a.getName();
145: String rm = r.getClassName() + "." + r.getName();
146: int comp = am.compareTo(rm);
147: if (comp == 0 && a.isMethod() && r.isMethod()) { // collect overloads
148: ams.add(a);
149: a = null;
150: rms.add(r);
151: r = null;
152: continue;
153: }
154:
155: if (!ams.isEmpty()) {
156: // simplest case first
157: if (ams.size() == 1 && rms.size() == 1) {
158: changed.add(new DeltaInfo((APIInfo) ams.get(0),
159: (APIInfo) rms.get(0)));
160: } else {
161: // dang, what to do now?
162: // TODO: modify deltainfo to deal with lists of added and removed
163: }
164: ams.clear();
165: rms.clear();
166: }
167:
168: int result = c.compare(a, r);
169: if (result < 0) {
170: a = null;
171: } else if (result > 0) {
172: r = null;
173: } else {
174: changed.add(new DeltaInfo(a, r));
175: a = null;
176: r = null;
177: }
178: }
179:
180: // now clean up added and removed by cleaning out the changed members
181: Iterator ci = changed.iterator();
182: while (ci.hasNext()) {
183: DeltaInfo di = (DeltaInfo) ci.next();
184: added.remove(di.added);
185: removed.remove(di.removed);
186: }
187:
188: Set tempAdded = new HashSet();
189: tempAdded.addAll(newData.set);
190: tempAdded.removeAll(removed);
191: TreeSet changedAdded = new TreeSet(APIInfo.defaultComparator());
192: changedAdded.addAll(tempAdded);
193:
194: Set tempRemoved = new HashSet();
195: tempRemoved.addAll(oldData.set);
196: tempRemoved.removeAll(added);
197: TreeSet changedRemoved = new TreeSet(APIInfo
198: .defaultComparator());
199: changedRemoved.addAll(tempRemoved);
200:
201: promoted = new TreeSet(APIInfo.defaultComparator());
202: obsoleted = new TreeSet(APIInfo.defaultComparator());
203: ai = changedAdded.iterator();
204: ri = changedRemoved.iterator();
205: a = r = null;
206: while ((a != null || ai.hasNext())
207: && (r != null || ri.hasNext())) {
208: if (a == null)
209: a = (APIInfo) ai.next();
210: if (r == null)
211: r = (APIInfo) ri.next();
212: int result = c.compare(a, r);
213: if (result < 0) {
214: a = null;
215: } else if (result > 0) {
216: r = null;
217: } else {
218: int change = statusChange(a, r);
219: if (change > 0) {
220: promoted.add(a);
221: } else if (change < 0) {
222: obsoleted.add(a);
223: }
224: a = null;
225: r = null;
226: }
227: }
228:
229: added = stripAndResort(added);
230: removed = stripAndResort(removed);
231: promoted = stripAndResort(promoted);
232: obsoleted = stripAndResort(obsoleted);
233: }
234:
235: private int statusChange(APIInfo lhs, APIInfo rhs) { // new. old
236: for (int i = 0; i < APIInfo.NUM_TYPES; ++i) {
237: if (lhs.get(i, true).equals(rhs.get(i, true)) == (i == APIInfo.STA)) {
238: return 0;
239: }
240: }
241: int lstatus = lhs.getVal(APIInfo.STA);
242: if (lstatus == APIInfo.STA_OBSOLETE
243: || lstatus == APIInfo.STA_DEPRECATED
244: || lstatus == APIInfo.STA_INTERNAL) {
245: return -1;
246: }
247: return 1;
248: }
249:
250: private boolean writeReport(String outFile, boolean html,
251: boolean internal) {
252: OutputStream os = System.out;
253: if (outFile != null) {
254: try {
255: os = new FileOutputStream(outFile);
256: } catch (FileNotFoundException e) {
257: RuntimeException re = new RuntimeException(e
258: .getMessage());
259: re.initCause(e);
260: throw re;
261: }
262: }
263:
264: PrintWriter pw = null;
265: try {
266: pw = new PrintWriter(new BufferedWriter(
267: new OutputStreamWriter(os, "UTF-8")));
268: } catch (UnsupportedEncodingException e) {
269: throw new IllegalStateException(); // UTF-8 should always be supported
270: }
271:
272: DateFormat fmt = new SimpleDateFormat("yyyy");
273: String year = fmt.format(new Date());
274: String title = "ICU4J API Comparison: " + oldData.name
275: + " with " + newData.name;
276: String info = "Contents generated by ReportAPI tool on "
277: + new Date().toString();
278: String copyright = "Copyright (C) "
279: + year
280: + ", International Business Machines Corporation, All Rights Reserved.";
281:
282: if (html) {
283: pw
284: .println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
285: pw.println("<html>");
286: pw.println("<head>");
287: pw
288: .println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">");
289: pw.println("<title>" + title + "</title>");
290: pw.println("<!-- Copyright " + year
291: + ", IBM, All Rights Reserved. -->");
292: pw.println("</head>");
293: pw.println("<body>");
294:
295: pw.println("<h1>" + title + "</h1>");
296:
297: pw.println();
298: pw.println("<hr/>");
299: pw.println("<h2>Removed from " + oldData.name + "</h2>");
300: if (removed.size() > 0) {
301: printResults(removed, pw, true, false);
302: } else {
303: pw.println("<p>(no API removed)</p>");
304: }
305:
306: pw.println();
307: pw.println("<hr/>");
308: if (internal) {
309: pw
310: .println("<h2>Withdrawn, Deprecated, or Obsoleted in "
311: + newData.name + "</h2>");
312: } else {
313: pw.println("<h2>Deprecated or Obsoleted in "
314: + newData.name + "</h2>");
315: }
316: if (obsoleted.size() > 0) {
317: printResults(obsoleted, pw, true, false);
318: } else {
319: pw.println("<p>(no API obsoleted)</p>");
320: }
321:
322: pw.println();
323: pw.println("<hr/>");
324: pw.println("<h2>Changed in " + newData.name
325: + " (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
335: + "</h2>");
336: if (promoted.size() > 0) {
337: printResults(promoted, pw, true, false);
338: } else {
339: pw.println("<p>(no API promoted)</p>");
340: }
341:
342: pw.println();
343: pw.println("<hr/>");
344: pw.println("<h2>Added in " + newData.name + "</h2>");
345: if (added.size() > 0) {
346: printResults(added, pw, true, false);
347: } else {
348: pw.println("<p>(no API added)</p>");
349: }
350:
351: pw.println("<hr/>");
352: pw.println("<p><i><font size=\"-1\">" + info + "<br/>"
353: + copyright + "</font></i></p>");
354: pw.println("</body>");
355: pw.println("</html>");
356: } else {
357: pw.println(title);
358: pw.println();
359: pw.println();
360:
361: pw.println("=== Removed from " + oldData.name + " ===");
362: if (removed.size() > 0) {
363: printResults(removed, pw, false, false);
364: } else {
365: pw.println("(no API removed)");
366: }
367:
368: pw.println();
369: pw.println();
370: if (internal) {
371: pw
372: .println("=== Withdrawn, Deprecated, or Obsoleted in "
373: + newData.name + " ===");
374: } else {
375: pw.println("=== Deprecated or Obsoleted in "
376: + newData.name + " ===");
377: }
378: if (obsoleted.size() > 0) {
379: printResults(obsoleted, pw, false, false);
380: } else {
381: pw.println("(no API obsoleted)");
382: }
383:
384: pw.println();
385: pw.println();
386: pw.println("=== Changed in " + newData.name
387: + " (old, new) ===");
388: if (changed.size() > 0) {
389: printResults(changed, pw, false, true);
390: } else {
391: pw.println("(no API changed)");
392: }
393:
394: pw.println();
395: pw.println();
396: pw.println("=== Promoted to stable in " + newData.name
397: + " ===");
398: if (promoted.size() > 0) {
399: printResults(promoted, pw, false, false);
400: } else {
401: pw.println("(no API promoted)");
402: }
403:
404: pw.println();
405: pw.println();
406: pw.println("=== Added in " + newData.name + " ===");
407: if (added.size() > 0) {
408: printResults(added, pw, false, false);
409: } else {
410: pw.println("(no API added)");
411: }
412:
413: pw.println();
414: pw.println("================");
415: pw.println(info);
416: pw.println(copyright);
417: }
418: pw.close();
419:
420: return false;
421: }
422:
423: private static void printResults(Collection c, PrintWriter pw,
424: boolean html, boolean isChangedAPIs) {
425: Iterator iter = c.iterator();
426: String pack = null;
427: String clas = null;
428: while (iter.hasNext()) {
429: APIInfo info = (APIInfo) iter.next();
430:
431: String packageName = info.getPackageName();
432: if (!packageName.equals(pack)) {
433: if (html) {
434: if (clas != null) {
435: pw.println("</ul>");
436: }
437: if (pack != null) {
438: pw.println("</ul>");
439: }
440: pw.println();
441: pw.println("<h3>Package " + packageName + "</h3>");
442: pw.print("<ul>");
443: } else {
444: if (pack != null) {
445: pw.println();
446: }
447: pw.println();
448: pw.println("Package " + packageName + ":");
449: }
450: pw.println();
451:
452: pack = packageName;
453: clas = null;
454: }
455:
456: if (!info.isClass()) {
457: String className = info.getClassName();
458: if (!className.equals(clas)) {
459: if (html) {
460: if (clas != null) {
461: pw.println("</ul>");
462: }
463: pw.println(className);
464: pw.println("<ul>");
465: } else {
466: pw.println(className);
467: }
468: clas = className;
469: }
470: }
471:
472: if (html) {
473: pw.print("<li>");
474: info.print(pw, isChangedAPIs, html);
475: pw.println("</li>");
476: } else {
477: info.println(pw, isChangedAPIs, html);
478: }
479: }
480:
481: if (html) {
482: if (clas != null) {
483: pw.println("</ul>");
484: }
485: if (pack != null) {
486: pw.println("</ul>");
487: }
488: }
489: pw.println();
490: }
491:
492: private static TreeSet stripAndResort(TreeSet t) {
493: stripClassInfo(t);
494: TreeSet r = new TreeSet(APIInfo.classFirstComparator());
495: r.addAll(t);
496: return r;
497: }
498:
499: private static void stripClassInfo(Collection c) {
500: // c is sorted with class info first
501: Iterator iter = c.iterator();
502: String cname = null;
503: while (iter.hasNext()) {
504: APIInfo info = (APIInfo) iter.next();
505: String className = info.getClassName();
506: if (cname != null) {
507: if (cname.equals(className)) {
508: iter.remove();
509: continue;
510: }
511: cname = null;
512: }
513: if (info.isClass()) {
514: cname = info.getName();
515: }
516: }
517: }
518: }
|