/* * Copyright 2006 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.doctool; import com.google.doctool.LinkResolver.ExtraClassResolver; import com.sun.javadoc.ClassDoc; import com.sun.javadoc.ConstructorDoc; import com.sun.javadoc.Doc; import com.sun.javadoc.DocErrorReporter; import com.sun.javadoc.ExecutableMemberDoc; import com.sun.javadoc.FieldDoc; import com.sun.javadoc.MemberDoc; import com.sun.javadoc.MethodDoc; import com.sun.javadoc.PackageDoc; import com.sun.javadoc.ParamTag; import com.sun.javadoc.Parameter; import com.sun.javadoc.ProgramElementDoc; import com.sun.javadoc.RootDoc; import com.sun.javadoc.SeeTag; import com.sun.javadoc.SourcePosition; import com.sun.javadoc.Tag; import com.sun.javadoc.Type; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.HashSet; import java.util.Iterator; import java.util.Stack; /** * Generates XML from Javadoc source, with particular idioms to make it possible * to translate into either expository doc or API doc. */ public class Booklet { private static final String OPT_BKCODE = "-bkcode"; private static final String OPT_BKDOCPKG = "-bkdocpkg"; private static final String OPT_BKOUT = "-bkout"; private static Booklet sBooklet; public static void main(String[] args) { // Strip off our arguments at the beginning. // com.sun.tools.javadoc.Main.execute(args); } public static int optionLength(String option) { if (option.equals(OPT_BKOUT)) { return 2; } else if (option.equals(OPT_BKDOCPKG)) { return 2; } else if (option.equals(OPT_BKCODE)) { return 1; } return 0; } public static String slurpSource(SourcePosition position) { BufferedReader br = null; try { br = new BufferedReader(new FileReader(position.file())); for (int i = 0, n = position.line() - 1; i < n; ++i) { br.readLine(); } StringBuffer lines = new StringBuffer(); String line = br.readLine(); int braceDepth = 0; int indent = -1; boolean seenSemiColonOrBrace = false; while (line != null) { if (indent == -1) { for (indent = 0; Character.isWhitespace(line.charAt(indent)); ++indent) { // just accumulate } } if (line.length() >= indent) { line = line.substring(indent); } lines.append(line + "\n"); for (int i = 0, n = line.length(); i < n; ++i) { char c = line.charAt(i); if (c == '{') { seenSemiColonOrBrace = true; ++braceDepth; } else if (c == '}') { --braceDepth; } else if (c == ';') { seenSemiColonOrBrace = true; } } if (braceDepth > 0 || !seenSemiColonOrBrace) { line = br.readLine(); } else { break; } } String code = lines.toString(); return code; } catch (Exception e) { e.printStackTrace(); } finally { try { if (br != null) { br.close(); } } catch (IOException e) { e.printStackTrace(); } } return ""; } public static boolean start(RootDoc rootDoc) { getBooklet().process(rootDoc); return true; } public static boolean validOptions(String[][] options, DocErrorReporter reporter) { return getBooklet().analyzeOptions(options, reporter); } private static Booklet getBooklet() { if (sBooklet == null) { sBooklet = new Booklet(); } return sBooklet; } private String outputPath; private HashSet packagesToGenerate; private RootDoc initialRootDoc; private String rootDocId; private boolean showCode; private HashSet standardTagKinds = new HashSet(); private Stack tagStack = new Stack(); private PrintWriter pw; public Booklet() { // Set up standard tags (to ignore during tag processing) // standardTagKinds.add("@see"); standardTagKinds.add("@serial"); standardTagKinds.add("@throws"); standardTagKinds.add("@param"); standardTagKinds.add("@id"); } private boolean analyzeOptions(String[][] options, DocErrorReporter reporter) { for (int i = 0, n = options.length; i < n; ++i) { if (options[i][0].equals(OPT_BKOUT)) { outputPath = options[i][1]; } else if (options[i][0].equals(OPT_BKDOCPKG)) { String[] packages = options[i][1].split(";"); packagesToGenerate = new HashSet(); for (int packageIndex = 0; packageIndex < packages.length; ++packageIndex) { packagesToGenerate.add(packages[packageIndex]); } } else if (options[i][0].equals(OPT_BKCODE)) { showCode = true; } } if (outputPath == null) { reporter.printError("You must specify an output directory with " + OPT_BKOUT); return false; } return true; } private void begin(String tag) { pw.print("<" + tag + ">"); tagStack.push(tag); } private void begin(String tag, String attr, String value) { pw.print("<" + tag + " " + attr + "='" + value + "'>"); tagStack.push(tag); } private void beginCDATA() { pw.print("<![CDATA["); } private void beginEndln(String tag) { pw.println("<" + tag + "/>"); } private void beginln(String tag) { pw.println(); begin(tag); } private void beginln(String tag, String attr, String value) { pw.println(); begin(tag, attr, value); } private void emitDescription(ClassDoc enclosing, Doc forWhat, Tag[] leadInline, Tag[] descInline) { emitJRELink(enclosing, forWhat); beginln("lead"); processTags(leadInline); endln(); beginln("description"); processTags(descInline); endln(); } private void emitIdentity(String id, String name) { beginln("id"); text(id); endln(); beginln("name"); text(name); endln(); } private void emitJRELink(ClassDoc enclosing, Doc doc) { String jreLink = "http://java.sun.com/j2se/1.5.0/docs/api/"; if (doc instanceof ClassDoc) { ClassDoc classDoc = (ClassDoc) doc; String pkg = classDoc.containingPackage().name(); if (!pkg.startsWith("java.")) { return; } String clazz = classDoc.name(); jreLink += pkg.replace('.', '/') + "/"; jreLink += clazz; jreLink += ".html"; } else if (doc instanceof ExecutableMemberDoc) { ExecutableMemberDoc execMemberDoc = (ExecutableMemberDoc) doc; String pkg = enclosing.containingPackage().name(); if (!pkg.startsWith("java.")) { return; } String clazz = enclosing.name(); String method = execMemberDoc.name(); String sig = execMemberDoc.signature(); jreLink += pkg.replace('.', '/') + "/"; jreLink += clazz; jreLink += ".html"; jreLink += "#"; jreLink += method; jreLink += sig; } else if (doc instanceof PackageDoc) { String pkg = doc.name(); if (!pkg.startsWith("java.")) { return; } jreLink += pkg.replace('.', '/') + "/package-summary.html"; } else if (doc instanceof FieldDoc) { FieldDoc fieldDoc = (FieldDoc) doc; String pkg = enclosing.containingPackage().name(); if (!pkg.startsWith("java.")) { return; } String clazz = fieldDoc.containingClass().name(); String field = fieldDoc.name(); jreLink += pkg.replace('.', '/') + "/"; jreLink += clazz; jreLink += ".html"; jreLink += "#"; jreLink += field; } // Add the link. // beginln("jre"); text(jreLink); endln(); } private void emitLocation(Doc doc) { Doc parent = getParentDoc(doc); if (parent != null) { beginln("location"); emitLocationLink(parent); endln(); } } private void emitLocationLink(Doc doc) { // Intentionally reverses the order. // String myId; String myTitle; if (doc instanceof MemberDoc) { MemberDoc memberDoc = (MemberDoc) doc; myId = getId(memberDoc); myTitle = memberDoc.name(); } else if (doc instanceof ClassDoc) { ClassDoc classDoc = (ClassDoc) doc; myId = getId(classDoc); myTitle = classDoc.name(); } else if (doc instanceof PackageDoc) { PackageDoc pkgDoc = (PackageDoc) doc; myId = getId(pkgDoc); myTitle = pkgDoc.name(); } else if (doc instanceof RootDoc) { myId = rootDocId; myTitle = initialRootDoc.name(); } else { throw new IllegalStateException( "Expected only a member, type, or package"); } Doc parent = getParentDoc(doc); if (parent != null) { emitLocationLink(parent); } beginln("link", "ref", myId); Tag[] titleTag = doc.tags("@title"); if (titleTag.length > 0) { myTitle = titleTag[0].text().trim(); } if (myTitle == null || myTitle.length() == 0) { myTitle = "[NO TITLE]"; } text(myTitle); endln(); } private void emitModifiers(ProgramElementDoc doc) { if (doc.isPrivate()) { beginEndln("isPrivate"); } else if (doc.isProtected()) { beginEndln("isProtected"); } else if (doc.isPublic()) { beginEndln("isPublic"); } else if (doc.isPackagePrivate()) { beginEndln("isPackagePrivate"); } if (doc.isStatic()) { beginEndln("isStatic"); } if (doc.isFinal()) { beginEndln("isFinal"); } if (doc instanceof MethodDoc) { MethodDoc methodDoc = (MethodDoc) doc; if (methodDoc.isAbstract()) { beginEndln("isAbstract"); } if (methodDoc.isSynchronized()) { beginEndln("isSynchronized"); } } } private void emitOutOfLineTags(Tag[] tags) { beginln("tags"); processTags(tags); endln(); } private void emitType(Type type) { ClassDoc typeAsClass = type.asClassDoc(); if (typeAsClass != null) { begin("type", "ref", getId(typeAsClass)); } else { begin("type"); } String typeName = type.typeName(); String dims = type.dimension(); text(typeName + dims); end(); } private void end() { pw.print("</" + tagStack.pop() + ">"); } private void endCDATA() { pw.print("]]>"); } private void endln() { end(); pw.println(); } private MethodDoc findMatchingInterfaceMethodDoc(ClassDoc[] interfaces, MethodDoc methodDoc) { if (interfaces != null) { // Look through the methods on superInterface for a matching methodDoc. // for (int intfIndex = 0; intfIndex < interfaces.length; ++intfIndex) { ClassDoc currentIntfDoc = interfaces[intfIndex]; MethodDoc[] intfMethodDocs = currentIntfDoc.methods(); for (int methodIndex = 0; methodIndex < intfMethodDocs.length; ++methodIndex) { MethodDoc intfMethodDoc = intfMethodDocs[methodIndex]; String methodDocName = methodDoc.name(); String intfMethodDocName = intfMethodDoc.name(); if (methodDocName.equals(intfMethodDocName)) { if (methodDoc.signature().equals(intfMethodDoc.signature())) { // It's a match! // return intfMethodDoc; } } } // Try the superinterfaces of this interface. // MethodDoc foundMethodDoc = findMatchingInterfaceMethodDoc( currentIntfDoc.interfaces(), methodDoc); if (foundMethodDoc != null) { return foundMethodDoc; } } } // Just didn't find it anywhere. Must not be based on an implemented // interface. // return null; } private ExtraClassResolver getExtraClassResolver(Tag tag) { if (tag.holder() instanceof PackageDoc) { return new ExtraClassResolver() { public ClassDoc findClass(String className) { return initialRootDoc.classNamed(className); } }; } return null; } private String getId(ClassDoc classDoc) { return classDoc.qualifiedName(); } private String getId(ExecutableMemberDoc memberDoc) { // Use the mangled name to look up a unique id (based on its hashCode). // String clazz = memberDoc.containingClass().qualifiedName(); String id = clazz + "#" + memberDoc.name() + memberDoc.signature(); return id; } private String getId(FieldDoc fieldDoc) { String clazz = fieldDoc.containingClass().qualifiedName(); String id = clazz + "#" + fieldDoc.name(); return id; } private String getId(MemberDoc memberDoc) { if (memberDoc.isMethod()) { return getId((MethodDoc) memberDoc); } else if (memberDoc.isConstructor()) { return getId((ConstructorDoc) memberDoc); } else if (memberDoc.isField()) { return getId((FieldDoc) memberDoc); } else { throw new RuntimeException("Unknown member type"); } } private String getId(PackageDoc packageDoc) { return packageDoc.name(); } private Doc getParentDoc(Doc doc) { if (doc instanceof MemberDoc) { MemberDoc memberDoc = (MemberDoc) doc; return memberDoc.containingClass(); } else if (doc instanceof ClassDoc) { ClassDoc classDoc = (ClassDoc) doc; Doc enclosingClass = classDoc.containingClass(); if (enclosingClass != null) { return enclosingClass; } else { return classDoc.containingPackage(); } } else if (doc instanceof PackageDoc) { return initialRootDoc; } else if (doc instanceof RootDoc) { return null; } else { throw new IllegalStateException( "Expected only a member, type, or package"); } } private boolean looksSynthesized(ExecutableMemberDoc memberDoc) { SourcePosition memberPos = memberDoc.position(); int memberLine = memberPos.line(); SourcePosition classPos = memberDoc.containingClass().position(); int classLine = classPos.line(); if (memberLine == classLine) { return true; } else { return false; } } private void process(ClassDoc enclosing, ClassDoc classDoc) { // Make sure it isn't a @skip-ped topic. // if (classDoc.tags("@skip").length > 0) { // This one is explicitly skipped right now. // return; } if (classDoc.isInterface()) { beginln("interface"); } else { beginln("class"); } emitIdentity(getId(classDoc), classDoc.name()); emitLocation(classDoc); emitDescription(enclosing, classDoc, classDoc.firstSentenceTags(), classDoc.inlineTags()); emitOutOfLineTags(classDoc.tags()); emitModifiers(classDoc); ClassDoc superclassDoc = classDoc.superclass(); if (superclassDoc != null) { beginln("superclass", "ref", getId(superclassDoc)); text(superclassDoc.name()); endln(); } ClassDoc[] superinterfacesDoc = classDoc.interfaces(); for (int i = 0; i < superinterfacesDoc.length; i++) { ClassDoc superinterfaceDoc = superinterfacesDoc[i]; beginln("superinterface", "ref", getId(superinterfaceDoc)); text(superinterfaceDoc.name()); endln(); } ClassDoc[] cda = classDoc.innerClasses(); for (int i = 0; i < cda.length; i++) { process(classDoc, cda[i]); } FieldDoc[] fda = classDoc.fields(); for (int i = 0; i < fda.length; i++) { process(classDoc, fda[i]); } ConstructorDoc[] ctorDocs = classDoc.constructors(); for (int i = 0; i < ctorDocs.length; i++) { process(classDoc, ctorDocs[i]); } MethodDoc[] methods = classDoc.methods(); for (int i = 0; i < methods.length; i++) { process(classDoc, methods[i]); } endln(); } private void process(ClassDoc enclosing, ExecutableMemberDoc memberDoc) { if (looksSynthesized(memberDoc)) { // Skip it. // return; } // Make sure it isn't a @skip-ped member. // if (memberDoc.tags("@skip").length > 0) { // This one is explicitly skipped right now. // return; } if (memberDoc instanceof MethodDoc) { beginln("method"); emitIdentity(getId(memberDoc), memberDoc.name()); emitLocation(memberDoc); // If this method is not explicitly documented, use the best inherited // one. // String rawComment = memberDoc.getRawCommentText(); if (rawComment.length() == 0) { // Switch out the member doc being used. // MethodDoc methodDoc = (MethodDoc) memberDoc; MethodDoc superMethodDoc = methodDoc.overriddenMethod(); if (superMethodDoc == null) { ClassDoc classDocToTry = methodDoc.containingClass(); while (classDocToTry != null) { // See if this is a method from an interface. // If so, borrow its description. // superMethodDoc = findMatchingInterfaceMethodDoc( classDocToTry.interfaces(), methodDoc); if (superMethodDoc != null) { break; } classDocToTry = classDocToTry.superclass(); } } if (superMethodDoc != null) { // Borrow the description from the superclass/superinterface. // memberDoc = superMethodDoc; } } } else if (memberDoc instanceof ConstructorDoc) { beginln("constructor"); emitIdentity(getId(memberDoc), memberDoc.containingClass().name()); emitLocation(memberDoc); } else { throw new IllegalStateException("What kind of executable member is this?"); } emitDescription(enclosing, memberDoc, memberDoc.firstSentenceTags(), memberDoc.inlineTags()); emitOutOfLineTags(memberDoc.tags()); emitModifiers(memberDoc); begin("flatSignature"); text(memberDoc.flatSignature()); end(); // Return type if it's a method // if (memberDoc instanceof MethodDoc) { emitType(((MethodDoc) memberDoc).returnType()); } // Parameters // beginln("params"); Parameter[] pda = memberDoc.parameters(); for (int i = 0; i < pda.length; i++) { Parameter pd = pda[i]; begin("param"); emitType(pd.type()); begin("name"); text(pd.name()); end(); end(); } endln(); // Exceptions thrown // ClassDoc[] tea = memberDoc.thrownExceptions(); if (tea.length > 0) { beginln("throws"); for (int i = 0; i < tea.length; ++i) { ClassDoc te = tea[i]; beginln("throw", "ref", getId(te)); text(te.name()); endln(); } endln(); } // Maybe show code // if (showCode) { SourcePosition pos = memberDoc.position(); if (pos != null) { beginln("code"); String source = slurpSource(pos); begin("pre", "class", "code"); beginCDATA(); text(source); endCDATA(); endln(); endln(); } } endln(); } private void process(ClassDoc enclosing, FieldDoc fieldDoc) { // Make sure it isn't @skip-ped. // if (fieldDoc.tags("@skip").length > 0) { // This one is explicitly skipped right now. // return; } String commentText = fieldDoc.commentText(); if (fieldDoc.isPrivate() && (commentText == null || commentText.length() == 0)) { return; } beginln("field"); emitIdentity(fieldDoc.qualifiedName(), fieldDoc.name()); emitLocation(fieldDoc); emitDescription(enclosing, fieldDoc, fieldDoc.firstSentenceTags(), fieldDoc.inlineTags()); emitOutOfLineTags(fieldDoc.tags()); emitModifiers(fieldDoc); emitType(fieldDoc.type()); endln(); } private void process(PackageDoc packageDoc) { beginln("package"); emitIdentity(packageDoc.name(), packageDoc.name()); emitLocation(packageDoc); emitDescription(null, packageDoc, packageDoc.firstSentenceTags(), packageDoc.inlineTags()); emitOutOfLineTags(packageDoc.tags()); // Top-level classes // ClassDoc[] cda = packageDoc.allClasses(); for (int i = 0; i < cda.length; i++) { ClassDoc cd = cda[i]; // Make sure we have source. // SourcePosition p = cd.position(); if (p == null || p.line() == 0) { // Skip this since it isn't ours (otherwise we would have source). // continue; } if (cd.containingClass() == null) { process(cd, cda[i]); } else { // Not a top-level class. // cd = cda[i]; } } endln(); } private void process(RootDoc rootDoc) { try { initialRootDoc = rootDoc; File outputFile = new File(outputPath); // Ignore result since the next line will fail if the directory doesn't // exist. outputFile.getParentFile().mkdirs(); FileWriter fw = new FileWriter(outputFile); pw = new PrintWriter(fw, true); beginln("booklet"); rootDocId = ""; String title = ""; Tag[] idTags = rootDoc.tags("@id"); if (idTags.length > 0) { rootDocId = idTags[0].text(); } else { initialRootDoc.printWarning("Expecting @id in an overview html doc; see -overview"); } Tag[] titleTags = rootDoc.tags("@title"); if (titleTags.length > 0) { title = titleTags[0].text(); } else { initialRootDoc.printWarning("Expecting @title in an overview html doc; see -overview"); } emitIdentity(rootDocId, title); emitLocation(rootDoc); emitDescription(null, rootDoc, rootDoc.firstSentenceTags(), rootDoc.inlineTags()); emitOutOfLineTags(rootDoc.tags()); // Create a list of the packages to iterate over. // HashSet packageNames = new HashSet(); ClassDoc[] cda = initialRootDoc.classes(); for (int i = 0; i < cda.length; i++) { ClassDoc cd = cda[i]; // Only top-level classes matter. // if (cd.containingClass() == null) { packageNames.add(cd.containingPackage().name()); } } // Packages // for (Iterator iter = packageNames.iterator(); iter.hasNext();) { String packageName = (String) iter.next(); // Only process this package if either no "docpkg" is set, or it is // included. // if (packagesToGenerate == null || packagesToGenerate.contains(packageName)) { PackageDoc pd = initialRootDoc.packageNamed(packageName); process(pd); } } endln(); } catch (Exception e) { e.printStackTrace(); initialRootDoc.printError("Caught exception: " + e.toString()); } } private void processSeeTag(SeeTag seeTag) { String ref = null; ClassDoc cd = null; PackageDoc pd = null; MemberDoc md = null; String title = null; // Check for HTML links if (seeTag.text().startsWith("<")) { // TODO: ignore for now return; } // Ordered: most-specific to least-specific if (null != (md = seeTag.referencedMember())) { ref = getId(md); } else if (null != (cd = seeTag.referencedClass())) { ref = getId(cd); // See if the target has a title. // Tag[] titleTag = cd.tags("@title"); if (titleTag.length > 0) { title = titleTag[0].text().trim(); if (title.length() == 0) { title = null; } } } else if (null != (pd = seeTag.referencedPackage())) { ref = getId(pd); } String label = seeTag.label(); // If there is a label, use it. if (label == null || label.trim().length() == 0) { // If there isn't a label, see if the @see target has a @title. // if (title != null) { label = title; } else { label = seeTag.text(); if (label.endsWith(".")) { label = label.substring(0, label.length() - 1); } // Rip off all but the last interesting part to prevent fully-qualified // names everywhere. // int last1 = label.lastIndexOf('.'); int last2 = label.lastIndexOf('#'); if (last2 > last1) { // Use the class name plus the member name. // label = label.substring(last1 + 1).replace('#', '.'); } else if (last1 != -1) { label = label.substring(last1 + 1); } if (label.charAt(0) == '.') { // Started with "#" so remove the dot. // label = label.substring(1); } } } if (ref != null) { begin("link", "ref", ref); text(label != null ? label.trim() : ""); end(); } else { initialRootDoc.printWarning(seeTag.position(), "Unverifiable cross-reference to '" + seeTag.text() + "'"); // The link probably won't work, but emit it anyway. begin("link"); text(label != null ? label.trim() : ""); end(); } } private void processTags(Tag[] tags) { for (int i = 0; i < tags.length; i++) { Tag tag = tags[i]; String tagKind = tag.kind(); if (tagKind.equals("Text")) { text(tag.text()); } else if (tagKind.equals("@see")) { processSeeTag((SeeTag) tag); } else if (tagKind.equals("@param")) { ParamTag paramTag = (ParamTag) tag; beginln("param"); begin("name"); text(paramTag.parameterName()); end(); begin("description"); processTags(paramTag.inlineTags()); end(); endln(); } else if (tagKind.equals("@example")) { ExtraClassResolver extraClassResolver = getExtraClassResolver(tag); SourcePosition pos = LinkResolver.resolveLink(tag, extraClassResolver); String source = slurpSource(pos); begin("pre", "class", "code"); beginCDATA(); text(source); endCDATA(); endln(); } else if (tagKind.equals("@gwt.include")) { String contents = ResourceIncluder.getResourceFromClasspathScrubbedForHTML(tag); begin("pre", "class", "code"); text(contents); endln(); } else if (!standardTagKinds.contains(tag.name())) { // Custom tag; pass it along other tag. // String tagName = tag.name().substring(1); begin(tagName); processTags(tag.inlineTags()); end(); } } } private void text(String s) { pw.print(s); } }