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
45package com.ibm.icu.dev.tool.docs;
46
47// standard release sdk won't work, need internal build to get access to javadoc
48import java.io.BufferedWriter;
49import java.io.FileOutputStream;
50import java.io.IOException;
51import java.io.OutputStream;
52import java.io.OutputStreamWriter;
53import java.util.Collection;
54import java.util.Iterator;
55import java.util.TreeSet;
56import java.util.regex.Pattern;
57import java.util.zip.GZIPOutputStream;
58import java.util.zip.ZipEntry;
59import java.util.zip.ZipOutputStream;
60
61import com.sun.javadoc.ClassDoc;
62import com.sun.javadoc.ConstructorDoc;
63import com.sun.javadoc.ExecutableMemberDoc;
64import com.sun.javadoc.FieldDoc;
65import com.sun.javadoc.LanguageVersion;
66import com.sun.javadoc.MemberDoc;
67import com.sun.javadoc.MethodDoc;
68import com.sun.javadoc.ProgramElementDoc;
69import com.sun.javadoc.RootDoc;
70import com.sun.javadoc.Tag;
71
72public 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
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
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     */
113    public static LanguageVersion languageVersion() {
114        return LanguageVersion.JAVA_1_5;
115    }
116
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
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
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
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
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
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
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
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
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
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
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
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
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