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