1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
3 /**
4  *******************************************************************************
5  * Copyright (C) 2004-2014, International Business Machines Corporation and    *
6  * others. All Rights Reserved.                                                *
7  *******************************************************************************
8  */
9 
10 /**
11  * Generate a list of ICU's public APIs, sorted by qualified name and signature
12  * public APIs are all non-internal, non-package apis in com.ibm.icu.[lang|math|text|util].
13  * For each API, list
14  * - public, package, protected, or private (PB PK PT PR)
15  * - static or non-static (STK NST)
16  * - final or non-final (FN NF)
17  * - synchronized or non-synchronized (SYN NSY)
18  * - stable, draft, deprecated, obsolete (ST DR DP OB)
19  * - abstract or non-abstract (AB NA)
20  * - constructor, member, field (C M F)
21  *
22  * Requires JDK 1.5 or later
23  *
24  * Sample compilation:
25  * c:/doug/java/jdk1.5/build/windows-i586/bin/javac *.java
26  *
27  * Sample execution
28  * c:/j2sdk1.5/bin/javadoc
29  *   -classpath c:/jd2sk1.5/lib/tools.jar
30  *   -doclet com.ibm.icu.dev.tool.docs.GatherAPIData
31  *   -docletpath c:/doug/icu4j/tools/build/out/lib/icu4j-build-tools.jar
32  *   -sourcepath c:/doug/icu4j/main/classes/core/src
33  *   -name "ICU4J 4.2"
34  *   -output icu4j42.api2
35  *   -gzip
36  *   -source 1.5
37  *   com.ibm.icu.lang com.ibm.icu.math com.ibm.icu.text com.ibm.icu.util
38  *
39  * todo: provide command-line control of filters of which subclasses/packages to process
40  * todo: record full inheritance hierarchy, not just immediate inheritance
41  * todo: allow for aliasing comparisons (force (pkg.)*class to be treated as though it
42  *       were in a different pkg/class hierarchy (facilitates comparison of icu4j and java)
43  */
44 
45 package com.ibm.icu.dev.tool.docs;
46 
47 // standard release sdk won't work, need internal build to get access to javadoc
48 import java.io.BufferedWriter;
49 import java.io.FileOutputStream;
50 import java.io.IOException;
51 import java.io.OutputStream;
52 import java.io.OutputStreamWriter;
53 import java.util.Collection;
54 import java.util.Iterator;
55 import java.util.TreeSet;
56 import java.util.regex.Pattern;
57 import java.util.zip.GZIPOutputStream;
58 import java.util.zip.ZipEntry;
59 import java.util.zip.ZipOutputStream;
60 
61 import com.sun.javadoc.ClassDoc;
62 import com.sun.javadoc.ConstructorDoc;
63 import com.sun.javadoc.ExecutableMemberDoc;
64 import com.sun.javadoc.FieldDoc;
65 import com.sun.javadoc.LanguageVersion;
66 import com.sun.javadoc.MemberDoc;
67 import com.sun.javadoc.MethodDoc;
68 import com.sun.javadoc.ProgramElementDoc;
69 import com.sun.javadoc.RootDoc;
70 import com.sun.javadoc.Tag;
71 
72 public class GatherAPIData {
73     RootDoc root;
74     TreeSet results;
75     String srcName = "Current"; // default source name
76     String output; // name of output file to write
77     String base; // strip this prefix
78     Pattern pat;
79     boolean zip;
80     boolean gzip;
81     boolean internal;
82     boolean version;
83 
optionLength(String option)84     public static int optionLength(String option) {
85         if (option.equals("-name")) {
86             return 2;
87         } else if (option.equals("-output")) {
88             return 2;
89         } else if (option.equals("-base")) {
90             return 2;
91         } else if (option.equals("-filter")) {
92             return 2;
93         } else if (option.equals("-zip")) {
94             return 1;
95         } else if (option.equals("-gzip")) {
96             return 1;
97         } else if (option.equals("-internal")) {
98             return 1;
99         } else if (option.equals("-version")) {
100             return 1;
101         }
102         return 0;
103     }
104 
start(RootDoc root)105     public static boolean start(RootDoc root) {
106         return new GatherAPIData(root).run();
107     }
108 
109     /**
110      * If you don't do this, javadoc treats enums like regular classes!
111      * doesn't matter if you pass -source 1.5 or not.
112      */
languageVersion()113     public static LanguageVersion languageVersion() {
114         return LanguageVersion.JAVA_1_5;
115     }
116 
GatherAPIData(RootDoc root)117     GatherAPIData(RootDoc root) {
118         this.root = root;
119 
120         String[][] options = root.options();
121         for (int i = 0; i < options.length; ++i) {
122             String opt = options[i][0];
123             if (opt.equals("-name")) {
124                 this.srcName = options[i][1];
125             } else if (opt.equals("-output")) {
126                 this.output = options[i][1];
127             } else if (opt.equals("-base")) {
128                 this.base = options[i][1]; // should not include '.'
129             } else if (opt.equals("-filter")) {
130                 this.pat = Pattern.compile(options[i][1], Pattern.CASE_INSENSITIVE);
131             } else if (opt.equals("-zip")) {
132                 this.zip = true;
133             } else if (opt.equals("-gzip")) {
134                 this.gzip = true;
135             } else if (opt.equals("-internal")) {
136                 this.internal = true;
137             } else if (opt.equals("-version")) {
138                 this.version = true;
139             }
140         }
141 
142         results = new TreeSet(APIInfo.defaultComparator());
143     }
144 
run()145     private boolean run() {
146         doDocs(root.classes());
147 
148         OutputStream os = System.out;
149         if (output != null) {
150             ZipOutputStream zos = null;
151             try {
152                 if (zip) {
153                     zos = new ZipOutputStream(new FileOutputStream(output + ".zip"));
154                     zos.putNextEntry(new ZipEntry(output));
155                     os = zos;
156                 } else if (gzip) {
157                     os = new GZIPOutputStream(new FileOutputStream(output + ".gz"));
158                 } else {
159                     os = new FileOutputStream(output);
160                 }
161             }
162             catch (IOException e) {
163                 RuntimeException re = new RuntimeException(e.getMessage());
164                 re.initCause(e);
165                 throw re;
166             }
167             finally {
168                 if (zos != null) {
169                     try {
170                         zos.close();
171                     } catch (Exception e) {
172                         // ignore
173                     }
174                 }
175             }
176         }
177 
178         BufferedWriter bw = null;
179         try {
180             OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
181             bw = new BufferedWriter(osw);
182 
183             // writing data file
184             bw.write(String.valueOf(APIInfo.VERSION) + APIInfo.SEP); // header version
185             bw.write(srcName + APIInfo.SEP); // source name
186             bw.write((base == null ? "" : base) + APIInfo.SEP); // base
187             bw.newLine();
188             writeResults(results, bw);
189             bw.close(); // should flush, close all, etc
190         } catch (IOException e) {
191             try { bw.close(); } catch (IOException e2) {}
192             RuntimeException re = new RuntimeException("write error: " + e.getMessage());
193             re.initCause(e);
194             throw re;
195         }
196 
197         return false;
198     }
199 
doDocs(ProgramElementDoc[] docs)200     private void doDocs(ProgramElementDoc[] docs) {
201         if (docs != null && docs.length > 0) {
202             for (int i = 0; i < docs.length; ++i) {
203                 doDoc(docs[i]);
204             }
205         }
206     }
207 
doDoc(ProgramElementDoc doc)208     private void doDoc(ProgramElementDoc doc) {
209         if (ignore(doc)) return;
210 
211         if (doc.isClass() || doc.isInterface()) {
212             ClassDoc cdoc = (ClassDoc)doc;
213             doDocs(cdoc.fields());
214             doDocs(cdoc.constructors());
215             doDocs(cdoc.methods());
216             doDocs(cdoc.enumConstants());
217             // don't call this to iterate over inner classes,
218             // root.classes already includes them
219             // doDocs(cdoc.innerClasses());
220         }
221 
222         APIInfo info = createInfo(doc);
223         if (info != null) {
224             results.add(info);
225         }
226     }
227 
228     // Sigh. Javadoc doesn't indicate when the compiler generates
229     // the values and valueOf enum methods.  The position of the
230     // method for these is not always the same as the position of
231     // the class, though it often is, so we can't use that.
232 
isIgnoredEnumMethod(ProgramElementDoc doc)233     private boolean isIgnoredEnumMethod(ProgramElementDoc doc) {
234         if (doc.isMethod() && doc.containingClass().isEnum()) {
235             // System.out.println("*** " + doc.qualifiedName() + " pos: " +
236             //                    doc.position().line() +
237             //                    " containined by: " +
238             //                    doc.containingClass().name() +
239             //                    " pos: " +
240             //                    doc.containingClass().position().line());
241             // return doc.position().line() == doc.containingClass().position().line();
242 
243             String name = doc.name();
244             // assume we don't have enums that overload these method names.
245             return "values".equals(name) || "valueOf".equals(name);
246         }
247         return false;
248     }
249 
250     // isSynthesized also doesn't seem to work.  Let's do this, documenting
251     // synthesized constructors for abstract classes is kind of weird.
252     // We can't actually tell if the constructor was synthesized or is
253     // actually in the docs, but this shouldn't matter.  We don't really
254     // care if we didn't properly document the draft status of
255     // default constructors for abstract classes.
256 
257     // Update: We mandate a no-arg synthetic constructor with explicit
258     // javadoc comments by the policy. So, we no longer ignore abstract
259     // class's no-arg constructor blindly. -Yoshito 2014-05-21
260 
isAbstractClassDefaultConstructor(ProgramElementDoc doc)261     private boolean isAbstractClassDefaultConstructor(ProgramElementDoc doc) {
262         return doc.isConstructor()
263             && doc.containingClass().isAbstract()
264             && "()".equals(((ConstructorDoc) doc).signature());
265     }
266 
267     private static final boolean IGNORE_NO_ARG_ABSTRACT_CTOR = false;
268 
ignore(ProgramElementDoc doc)269     private boolean ignore(ProgramElementDoc doc) {
270         if (doc == null) return true;
271         if (doc.isPrivate() || doc.isPackagePrivate()) return true;
272         if (doc instanceof MemberDoc && ((MemberDoc)doc).isSynthetic()) return true;
273         if (doc.qualifiedName().indexOf(".misc") != -1) {
274             System.out.println("misc: " + doc.qualifiedName()); return true;
275         }
276         if (isIgnoredEnumMethod(doc)) {
277             return true;
278         }
279 
280         if (IGNORE_NO_ARG_ABSTRACT_CTOR && isAbstractClassDefaultConstructor(doc)) {
281             return true;
282         }
283 
284         if (false && doc.qualifiedName().indexOf("LocaleDisplayNames") != -1) {
285           System.err.print("*** " + doc.qualifiedName() + ":");
286           if (doc.isClass()) System.err.print(" class");
287           if (doc.isConstructor()) System.err.print(" constructor");
288           if (doc.isEnum()) System.err.print(" enum");
289           if (doc.isEnumConstant()) System.err.print(" enum_constant");
290           if (doc.isError()) System.err.print(" error");
291           if (doc.isException()) System.err.print(" exception");
292           if (doc.isField()) System.err.print(" field");
293           if (doc.isInterface()) System.err.print(" interface");
294           if (doc.isMethod()) System.err.print(" method");
295           if (doc.isOrdinaryClass()) System.err.print(" ordinary_class");
296           System.err.println();
297         }
298 
299         if (!internal) { // debug
300             Tag[] tags = doc.tags();
301             for (int i = 0; i < tags.length; ++i) {
302                 if (tagKindIndex(tags[i].kind()) == INTERNAL) { return true; }
303             }
304         }
305         if (pat != null && (doc.isClass() || doc.isInterface())) {
306             if (!pat.matcher(doc.name()).matches()) {
307                 return true;
308             }
309         }
310         return false;
311     }
312 
writeResults(Collection c, BufferedWriter w)313     private static void writeResults(Collection c, BufferedWriter w) {
314         Iterator iter = c.iterator();
315         while (iter.hasNext()) {
316             APIInfo info = (APIInfo)iter.next();
317             info.writeln(w);
318         }
319     }
320 
trimBase(String arg)321     private String trimBase(String arg) {
322         if (base != null) {
323             for (int n = arg.indexOf(base); n != -1; n = arg.indexOf(base, n)) {
324                 arg = arg.substring(0, n) + arg.substring(n+base.length());
325             }
326         }
327         return arg;
328     }
329 
createInfo(ProgramElementDoc doc)330     public APIInfo createInfo(ProgramElementDoc doc) {
331 
332         // Doc. name
333         // Doc. isField, isMethod, isConstructor, isClass, isInterface
334         // ProgramElementDoc. containingClass, containingPackage
335         // ProgramElementDoc. isPublic, isProtected, isPrivate, isPackagePrivate
336         // ProgramElementDoc. isStatic, isFinal
337         // MemberDoc.isSynthetic
338         // ExecutableMemberDoc isSynchronized, signature
339         // Type.toString() // e.g. "String[][]"
340         // ClassDoc.isAbstract, superClass, interfaces, fields, methods, constructors, innerClasses
341         // FieldDoc type
342         // ConstructorDoc qualifiedName
343         // MethodDoc isAbstract, returnType
344 
345         APIInfo info = new APIInfo();
346         if (version) {
347             info.includeStatusVersion(true);
348         }
349 
350         // status
351         String[] version = new String[1];
352         info.setType(APIInfo.STA, tagStatus(doc, version));
353         info.setStatusVersion(version[0]);
354 
355         // visibility
356         if (doc.isPublic()) {
357             info.setPublic();
358         } else if (doc.isProtected()) {
359             info.setProtected();
360         } else if (doc.isPrivate()) {
361             info.setPrivate();
362         } else {
363             // default is package
364         }
365 
366         // static
367         if (doc.isStatic()) {
368             info.setStatic();
369         } else {
370             // default is non-static
371         }
372 
373         // final
374         if (doc.isFinal() && !doc.isEnum()) {
375             info.setFinal();
376         } else {
377             // default is non-final
378         }
379 
380         // type
381         if (doc.isField()) {
382             info.setField();
383         } else if (doc.isMethod()) {
384             info.setMethod();
385         } else if (doc.isConstructor()) {
386             info.setConstructor();
387         } else if (doc.isClass() || doc.isInterface()) {
388             if (doc.isEnum()) {
389                 info.setEnum();
390             } else {
391                 info.setClass();
392             }
393         } else if (doc.isEnumConstant()) {
394             info.setEnumConstant();
395         }
396 
397         info.setPackage(trimBase(doc.containingPackage().name()));
398 
399         String className = (doc.isClass() || doc.isInterface() || (doc.containingClass() == null))
400                 ? ""
401                 : doc.containingClass().name();
402         info.setClassName(className);
403 
404         String name = doc.name();
405         if (doc.isConstructor()) {
406             // Workaround for Javadoc incompatibility between 7 and 8.
407             // Javadoc 7 prepends enclosing class name for a nested
408             // class's constructor. We need to generate the same format
409             // because existing ICU API signature were generated with
410             // Javadoc 7 or older verions.
411             int dotIdx = className.lastIndexOf('.');
412             if (!name.contains(".") && dotIdx > 0) {
413                 name = className.substring(0, dotIdx + 1) + name;
414             }
415         }
416         info.setName(name);
417 
418         if (doc instanceof FieldDoc) {
419             FieldDoc fdoc = (FieldDoc)doc;
420             info.setSignature(trimBase(fdoc.type().toString()));
421         } else if (doc instanceof ClassDoc) {
422             ClassDoc cdoc = (ClassDoc)doc;
423 
424             if (cdoc.isClass() && cdoc.isAbstract()) {
425                 // interfaces are abstract by default, don't mark them as abstract
426                 info.setAbstract();
427             }
428 
429             StringBuffer buf = new StringBuffer();
430             if (cdoc.isClass()) {
431                 buf.append("extends ");
432                 buf.append(cdoc.superclassType().toString());
433             }
434             ClassDoc[] imp = cdoc.interfaces();
435             if (imp != null && imp.length > 0) {
436                 if (buf.length() > 0) {
437                     buf.append(" ");
438                 }
439                 buf.append("implements");
440                 for (int i = 0; i < imp.length; ++i) {
441                     if (i != 0) {
442                         buf.append(",");
443                     }
444                     buf.append(" ");
445                     buf.append(imp[i].qualifiedName());
446                 }
447             }
448             info.setSignature(trimBase(buf.toString()));
449         } else {
450             ExecutableMemberDoc emdoc = (ExecutableMemberDoc)doc;
451             if (emdoc.isSynchronized()) {
452                 info.setSynchronized();
453             }
454 
455             if (doc instanceof MethodDoc) {
456                 MethodDoc mdoc = (MethodDoc)doc;
457                 if (mdoc.isAbstract()) {
458                     // Workaround for Javadoc incompatibility between 7 and 8.
459                     // isAbstract() returns false for a method in an interface
460                     // on Javadoc 7, while Javadoc 8 returns true. Because existing
461                     // API signature data files were generated before, we do not
462                     // set abstract if a method is in an interface.
463                     if (!mdoc.containingClass().isInterface()) {
464                         info.setAbstract();
465                     }
466                 }
467                 info.setSignature(trimBase(mdoc.returnType().toString() + emdoc.signature()));
468             } else {
469                 // constructor
470                 info.setSignature(trimBase(emdoc.signature()));
471             }
472         }
473 
474         return info;
475     }
476 
tagStatus(final ProgramElementDoc doc, String[] version)477     private int tagStatus(final ProgramElementDoc doc, String[] version) {
478         class Result {
479             boolean deprecatedFlag = false;
480             int res = -1;
481             void set(int val) {
482                 if (res != -1) {
483                     boolean isValid = true;
484                     if (val == APIInfo.STA_DEPRECATED) {
485                         // @internal and @obsolete should be always used along with @deprecated.
486                         // no change for status
487                         isValid = (res == APIInfo.STA_INTERNAL || res == APIInfo.STA_OBSOLETE);
488                         deprecatedFlag = true;
489                     } else if (val == APIInfo.STA_INTERNAL) {
490                         // @deprecated should be always used along with @internal.
491                         // update status
492                         if (res == APIInfo.STA_DEPRECATED) {
493                             res = val;  // APIInfo.STA_INTERNAL
494                         } else {
495                             isValid = false;
496                         }
497                     } else if (val == APIInfo.STA_OBSOLETE) {
498                         // @deprecated should be always used along with @obsolete.
499                         // update status
500                         if (res == APIInfo.STA_DEPRECATED) {
501                             res = val;  // APIInfo.STA_OBSOLETE
502                         } else {
503                             isValid = false;
504                         }
505                     } else {
506                         // two different status tags must not co-exist, except for
507                         // following two cases:
508                         // 1. @internal and @deprecated
509                         // 2. @obsolete and @deprecated
510                         isValid = false;
511                     }
512                     if (!isValid) {
513                         System.err.println("bad doc: " + doc + " both: "
514                                            + APIInfo.getTypeValName(APIInfo.STA, res) + " and: "
515                                            + APIInfo.getTypeValName(APIInfo.STA, val));
516                         return;
517                     }
518                 } else {
519                     // ok to replace with new tag
520                     res = val;
521                     if (val == APIInfo.STA_DEPRECATED) {
522                         deprecatedFlag = true;
523                     }
524                 }
525             }
526             int get() {
527                 if (res == -1) {
528                     System.err.println("warning: no tag for " + doc);
529                     return 0;
530                 } else if (res == APIInfo.STA_INTERNAL && !deprecatedFlag) {
531                     System.err.println("warning: no @deprecated tag for @internal API: " + doc);
532                 }
533                 return res;
534             }
535         }
536 
537         Tag[] tags = doc.tags();
538         Result result = new Result();
539         String statusVer = "";
540         for (int i = 0; i < tags.length; ++i) {
541             Tag tag = tags[i];
542 
543             String kind = tag.kind();
544             int ix = tagKindIndex(kind);
545 
546             switch (ix) {
547             case INTERNAL:
548                 result.set(internal ? APIInfo.STA_INTERNAL : -2); // -2 for legacy compatibility
549                 statusVer = getStatusVersion(tag);
550                 break;
551 
552             case DRAFT:
553                 result.set(APIInfo.STA_DRAFT);
554                 statusVer = getStatusVersion(tag);
555                 break;
556 
557             case STABLE:
558                 result.set(APIInfo.STA_STABLE);
559                 statusVer = getStatusVersion(tag);
560                 break;
561 
562             case DEPRECATED:
563                 result.set(APIInfo.STA_DEPRECATED);
564                 statusVer = getStatusVersion(tag);
565                 break;
566 
567             case OBSOLETE:
568                 result.set(APIInfo.STA_OBSOLETE);
569                 statusVer = getStatusVersion(tag);
570                 break;
571 
572             case SINCE:
573             case EXCEPTION:
574             case VERSION:
575             case UNKNOWN:
576             case AUTHOR:
577             case SEE:
578             case PARAM:
579             case RETURN:
580             case THROWS:
581             case SERIAL:
582                 break;
583 
584             default:
585                 throw new RuntimeException("unknown index " + ix + " for tag: " + kind);
586             }
587         }
588 
589         if (version != null) {
590             version[0] = statusVer;
591         }
592         return result.get();
593     }
594 
getStatusVersion(Tag tag)595     private String getStatusVersion(Tag tag) {
596         String text = tag.text();
597         if (text != null && text.length() > 0) {
598             // Extract version string
599             int start = -1;
600             int i = 0;
601             for (; i < text.length(); i++) {
602                 char ch = text.charAt(i);
603                 if (ch == '.' || (ch >= '0' && ch <= '9')) {
604                     if (start == -1) {
605                         start = i;
606                     }
607                 } else if (start != -1) {
608                     break;
609                 }
610             }
611             if (start != -1) {
612                 return text.substring(start, i);
613             }
614         }
615         return "";
616     }
617 
618     private static final int UNKNOWN = -1;
619     private static final int INTERNAL = 0;
620     private static final int DRAFT = 1;
621     private static final int STABLE = 2;
622     private static final int SINCE = 3;
623     private static final int DEPRECATED = 4;
624     private static final int AUTHOR = 5;
625     private static final int SEE = 6;
626     private static final int VERSION = 7;
627     private static final int PARAM = 8;
628     private static final int RETURN = 9;
629     private static final int THROWS = 10;
630     private static final int OBSOLETE = 11;
631     private static final int EXCEPTION = 12;
632     private static final int SERIAL = 13;
633 
tagKindIndex(String kind)634     private static int tagKindIndex(String kind) {
635         final String[] tagKinds = {
636             "@internal", "@draft", "@stable", "@since", "@deprecated", "@author", "@see",
637             "@version", "@param", "@return", "@throws", "@obsolete", "@exception", "@serial"
638         };
639 
640         for (int i = 0; i < tagKinds.length; ++i) {
641             if (kind.equals(tagKinds[i])) {
642                 return i;
643             }
644         }
645         return UNKNOWN;
646     }
647 }
648