org.mule.devkit.doclet.Doclava.java Source code

Java tutorial

Introduction

Here is the source code for org.mule.devkit.doclet.Doclava.java

Source

/*
 * Copyright (C) 2011 MuleSoft Inc.
 * Copyright (C) 2010 The Android Open Source Project
 *
 * 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 org.mule.devkit.doclet;

import com.google.clearsilver.jsilver.JSilver;
import com.google.clearsilver.jsilver.data.Data;
import com.google.clearsilver.jsilver.resourceloader.ClassResourceLoader;
import com.google.clearsilver.jsilver.resourceloader.CompositeResourceLoader;
import com.google.clearsilver.jsilver.resourceloader.FileSystemResourceLoader;
import com.google.clearsilver.jsilver.resourceloader.ResourceLoader;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.LanguageVersion;
import com.sun.javadoc.MemberDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Type;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.mule.devkit.doclet.markdown.MarkdownProcessor;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.jar.JarFile;

public class Doclava {
    public static final int SHOW_PUBLIC = 0x00000001;
    public static final int SHOW_PROTECTED = 0x00000003;
    public static final int SHOW_PACKAGE = 0x00000007;
    public static final int SHOW_PRIVATE = 0x0000000f;
    public static final int SHOW_HIDDEN = 0x0000001f;

    public static int showLevel = SHOW_PROTECTED;

    public static final String javadocDir = "java/";
    public static final String muleXmlDir = "mule/";
    public static final String guideDir = "guide/";
    public static String assetsOutputDir = "assets";
    public static String htmlExtension;

    public static RootDoc root;
    public static ArrayList<String[]> mHDFData = new ArrayList<String[]>();
    public static ArrayList<String[]> mMarkdown = new ArrayList<String[]>();
    public static Map<Character, String> escapeChars = new HashMap<Character, String>();
    public static String title = "";
    public static SinceTagger sinceTagger = new SinceTagger();
    public static HashSet<String> knownTags = new HashSet<String>();
    public static FederationTagger federationTagger = new FederationTagger();
    private static boolean generateDocs = true;
    private static boolean generateSources = false;
    private static boolean parseComments = false;
    public static String apiVersion = null;

    public static JSilver jSilver = null;

    public static boolean checkLevel(int level) {
        return (showLevel & level) == level;
    }

    /**
     * Returns true if we should parse javadoc comments,
     * reporting errors in the process.
     */
    public static boolean parseComments() {
        return generateDocs || parseComments;
    }

    public static boolean checkLevel(boolean pub, boolean prot, boolean pkgp, boolean priv, boolean hidden) {
        if (hidden && !checkLevel(SHOW_HIDDEN)) {
            return false;
        }
        if (pub && checkLevel(SHOW_PUBLIC)) {
            return true;
        }
        if (prot && checkLevel(SHOW_PROTECTED)) {
            return true;
        }
        if (pkgp && checkLevel(SHOW_PACKAGE)) {
            return true;
        }
        if (priv && checkLevel(SHOW_PRIVATE)) {
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        com.sun.tools.javadoc.Main.execute(args);
    }

    public static boolean start(RootDoc r) {
        String keepListFile = null;
        String proofreadFile = null;
        String todoFile = null;
        String stubsDir = null;
        // Create the dependency graph for the stubs directory
        String apiFile = null;
        HashSet<String> stubPackages = null;
        ArrayList<String> knownTagsFiles = new ArrayList<String>();

        root = r;

        String[][] options = r.options();
        for (String[] a : options) {
            if (a[0].equals("-d")) {
                ClearPage.outputDir = a[1];
            } else if (a[0].equals("-templatedir")) {
                ClearPage.addTemplateDir(a[1]);
            } else if (a[0].equals("-hdf")) {
                mHDFData.add(new String[] { a[1], a[2] });
            } else if (a[0].equals("-markdown")) {
                mMarkdown.add(new String[] { a[1], a[2], a[3] });
            } else if (a[0].equals("-knowntags")) {
                knownTagsFiles.add(a[1]);
            } else if (a[0].equals("-toroot")) {
                ClearPage.toroot = a[1];
            } else if (a[0].equals("-title")) {
                Doclava.title = a[1];
            } else if (a[0].equals("-werror")) {
                Errors.setWarningsAreErrors(true);
            } else if (a[0].equals("-error") || a[0].equals("-warning") || a[0].equals("-hide")) {
                try {
                    int level = -1;
                    if (a[0].equals("-error")) {
                        level = Errors.ERROR;
                    } else if (a[0].equals("-warning")) {
                        level = Errors.WARNING;
                    } else if (a[0].equals("-hide")) {
                        level = Errors.HIDDEN;
                    }
                    Errors.setErrorLevel(Integer.parseInt(a[1]), level);
                } catch (NumberFormatException e) {
                    // already printed below
                    return false;
                }
            } else if (a[0].equals("-keeplist")) {
                keepListFile = a[1];
            } else if (a[0].equals("-proofread")) {
                proofreadFile = a[1];
            } else if (a[0].equals("-todo")) {
                todoFile = a[1];
            } else if (a[0].equals("-public")) {
                showLevel = SHOW_PUBLIC;
            } else if (a[0].equals("-protected")) {
                showLevel = SHOW_PROTECTED;
            } else if (a[0].equals("-package")) {
                showLevel = SHOW_PACKAGE;
            } else if (a[0].equals("-private")) {
                showLevel = SHOW_PRIVATE;
            } else if (a[0].equals("-hidden")) {
                showLevel = SHOW_HIDDEN;
            } else if (a[0].equals("-stubs")) {
                stubsDir = a[1];
            } else if (a[0].equals("-stubpackages")) {
                stubPackages = new HashSet<String>();
                for (String pkg : a[1].split(":")) {
                    stubPackages.add(pkg);
                }
            } else if (a[0].equals("-apixml")) {
                apiFile = a[1];
            } else if (a[0].equals("-nodocs")) {
                generateDocs = false;
            } else if (a[0].equals("-parsecomments")) {
                parseComments = true;
            } else if (a[0].equals("-since")) {
                sinceTagger.addVersion(a[1], a[2]);
            } else if (a[0].equals("-federate")) {
                try {
                    String name = a[1];
                    URL federationURL = new URL(a[2]);
                    federationTagger.addSiteUrl(name, federationURL);
                } catch (MalformedURLException e) {
                    System.err.println("Could not parse URL for federation: " + a[1]);
                    return false;
                }
            } else if (a[0].equals("-federationxml")) {
                String name = a[1];
                String file = a[2];
                federationTagger.addSiteXml(name, file);
            } else if (a[0].equals("-apiversion")) {
                apiVersion = a[1];
            } else if (a[0].equals("-assetsdir")) {
                assetsOutputDir = a[1];
            } else if (a[0].equals("-generatesources")) {
                generateSources = true;
            }
        }

        if (!readKnownTagsFiles(knownTags, knownTagsFiles)) {
            return false;
        }

        // Set up the data structures
        Converter.makeInfo(r);

        // Stubs and xml
        final File currentApiFile;
        if (!generateDocs && apiFile != null) {
            currentApiFile = new File(apiFile);
        } else if (generateDocs) {
            currentApiFile = new File(ensureSlash(ClearPage.outputDir) + javadocDir + FederatedSite.XML_API_PATH);
        } else {
            currentApiFile = null;
        }

        Stubs.writeStubsAndXml(stubsDir, currentApiFile, stubPackages);

        if (generateDocs && apiFile != null) {
            ClearPage.copyFile(currentApiFile, new File(apiFile));
        }

        // Reference documentation
        if (generateDocs) {
            ClearPage.addBundledTemplateDir("assets/customizations");
            ClearPage.addBundledTemplateDir("assets/templates");

            List<ResourceLoader> resourceLoaders = new ArrayList<ResourceLoader>();
            List<String> templates = ClearPage.getTemplateDirs();
            for (String tmpl : templates) {
                resourceLoaders.add(new FileSystemResourceLoader(tmpl));
            }

            templates = ClearPage.getBundledTemplateDirs();
            for (String tmpl : templates) {
                resourceLoaders.add(new ClassResourceLoader(Doclava.class, '/' + tmpl));
            }

            ResourceLoader compositeResourceLoader = new CompositeResourceLoader(resourceLoaders);
            jSilver = new JSilver(compositeResourceLoader);

            if (!Doclava.readTemplateSettings()) {
                return false;
            }

            long startTime = System.nanoTime();

            // Use current version information
            if (apiVersion != null && sinceTagger.hasVersions()) {
                sinceTagger.addVersion(currentApiFile.getAbsolutePath(), apiVersion);
            }

            // Apply @since tags from the XML file
            sinceTagger.tagAll(Converter.rootClasses());

            // Apply details of federated documentation
            federationTagger.tagAll(Converter.rootClasses());

            // Files for proofreading
            if (proofreadFile != null) {
                Proofread.initProofread(proofreadFile);
            }
            if (todoFile != null) {
                TodoFile.writeTodoFile(todoFile);
            }

            writeAssets();

            // Navigation tree
            NavTree.writeNavTree(assetsOutputDir);

            // Write Markdown files
            writeMarkdowns();

            // Mule
            writeModules(muleXmlDir + "modules" + htmlExtension);

            // Packages Pages
            writePackages(javadocDir + "packages" + htmlExtension);

            // Classes
            writeClassLists();
            writeClasses();
            writeHierarchy();
            // writeKeywords();

            // Lists for JavaScript
            writeLists();
            if (keepListFile != null) {
                writeKeepList(keepListFile);
            }

            // Index page
            writeIndex();

            // Installation page
            writeInstall();

            Proofread.finishProofread(proofreadFile);

            long time = System.nanoTime() - startTime;
            System.out.println(
                    "Mule DevKit took " + (time / 1000000000) + " sec. to write docs to " + ClearPage.outputDir);
        }

        Errors.printErrors();

        return !Errors.hadError;
    }

    private static void writeIndex() {
        Data data = makeHDF();
        ClearPage.write(data, "index.cs", "index" + htmlExtension);
    }

    private static void writeInstall() {
        Data data = makeHDF();
        ClearPage.write(data, "install.cs", guideDir + "install" + htmlExtension);
    }

    private static boolean readTemplateSettings() {
        Data data = makeHDF();

        // The .html extension is hard-coded in several .cs files,
        // and so you cannot currently set it as a property.
        htmlExtension = ".html";
        // htmlExtension = data.getValue("template.extension", ".html");
        int i = 0;
        while (true) {
            String k = data.getValue("template.escape." + i + ".key", "");
            String v = data.getValue("template.escape." + i + ".value", "");
            if ("".equals(k)) {
                break;
            }
            if (k.length() != 1) {
                System.err.println("template.escape." + i + ".key must have a length of 1: " + k);
                return false;
            }
            escapeChars.put(k.charAt(0), v);
            i++;
        }
        return true;
    }

    private static boolean readKnownTagsFiles(HashSet<String> knownTags, ArrayList<String> knownTagsFiles) {
        for (String fn : knownTagsFiles) {
            BufferedReader in = null;
            try {
                in = new BufferedReader(new FileReader(fn));
                int lineno = 0;
                boolean fail = false;
                while (true) {
                    lineno++;
                    String line = in.readLine();
                    if (line == null) {
                        break;
                    }
                    line = line.trim();
                    if (line.length() == 0) {
                        continue;
                    } else if (line.charAt(0) == '#') {
                        continue;
                    }
                    String[] words = line.split("\\s+", 2);
                    if (words.length == 2) {
                        if (words[1].charAt(0) != '#') {
                            System.err.println(fn + ":" + lineno + ": Only one tag allowed per line: " + line);
                            fail = true;
                            continue;
                        }
                    }
                    knownTags.add(words[0]);
                }
                if (fail) {
                    return false;
                }
            } catch (IOException ex) {
                System.err.println("Error reading file: " + fn + " (" + ex.getMessage() + ")");
                return false;
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException ignored) {
                    }
                }
            }
        }
        return true;
    }

    public static String escape(String s) {
        if (escapeChars.size() == 0) {
            return s;
        }
        StringBuffer b = null;
        int begin = 0;
        final int N = s.length();
        for (int i = 0; i < N; i++) {
            char c = s.charAt(i);
            String mapped = escapeChars.get(c);
            if (mapped != null) {
                if (b == null) {
                    b = new StringBuffer(s.length() + mapped.length());
                }
                if (begin != i) {
                    b.append(s.substring(begin, i));
                }
                b.append(mapped);
                begin = i + 1;
            }
        }
        if (b != null) {
            if (begin != N) {
                b.append(s.substring(begin, N));
            }
            return b.toString();
        }
        return s;
    }

    public static void setPageTitle(Data data, String title) {
        String s = title;
        if (Doclava.title.length() > 0) {
            s += " - " + Doclava.title;
        }
        data.setValue("page.title", s);
    }

    public static LanguageVersion languageVersion() {
        return LanguageVersion.JAVA_1_5;
    }

    public static int optionLength(String option) {
        if (option.equals("-d")) {
            return 2;
        }
        if (option.equals("-templatedir")) {
            return 2;
        }
        if (option.equals("-hdf")) {
            return 3;
        }
        if (option.equals("-markdown")) {
            return 4;
        }
        if (option.equals("-knowntags")) {
            return 2;
        }
        if (option.equals("-toroot")) {
            return 2;
        }
        if (option.equals("-samplecode")) {
            return 4;
        }
        if (option.equals("-title")) {
            return 2;
        }
        if (option.equals("-werror")) {
            return 1;
        }
        if (option.equals("-hide")) {
            return 2;
        }
        if (option.equals("-warning")) {
            return 2;
        }
        if (option.equals("-error")) {
            return 2;
        }
        if (option.equals("-keeplist")) {
            return 2;
        }
        if (option.equals("-proofread")) {
            return 2;
        }
        if (option.equals("-todo")) {
            return 2;
        }
        if (option.equals("-public")) {
            return 1;
        }
        if (option.equals("-protected")) {
            return 1;
        }
        if (option.equals("-package")) {
            return 1;
        }
        if (option.equals("-private")) {
            return 1;
        }
        if (option.equals("-hidden")) {
            return 1;
        }
        if (option.equals("-stubs")) {
            return 2;
        }
        if (option.equals("-stubpackages")) {
            return 2;
        }
        if (option.equals("-sdkvalues")) {
            return 2;
        }
        if (option.equals("-apixml")) {
            return 2;
        }
        if (option.equals("-nodocs")) {
            return 1;
        }
        if (option.equals("-parsecomments")) {
            return 1;
        }
        if (option.equals("-since")) {
            return 3;
        }
        if (option.equals("-offlinemode")) {
            return 1;
        }
        if (option.equals("-federate")) {
            return 3;
        }
        if (option.equals("-federationxml")) {
            return 3;
        }
        if (option.equals("-apiversion")) {
            return 2;
        }
        if (option.equals("-assetsdir")) {
            return 2;
        }
        if (option.equals("-generatesources")) {
            return 1;
        }
        return 0;
    }

    public static boolean validOptions(String[][] options, DocErrorReporter r) {
        for (String[] a : options) {
            if (a[0].equals("-error") || a[0].equals("-warning") || a[0].equals("-hide")) {
                try {
                    Integer.parseInt(a[1]);
                } catch (NumberFormatException e) {
                    r.printError("bad -" + a[0] + " value must be a number: " + a[1]);
                    return false;
                }
            }
        }

        return true;
    }

    public static Data makeHDF() {
        Data data = jSilver.createData();

        for (String[] p : mHDFData) {
            data.setValue(p[0], p[1]);
        }

        data.setValue("tabs.0.id", "guide");
        data.setValue("tabs.0.title", "Install Guide");
        data.setValue("tabs.0.link", guideDir + "install.html");

        int i = 0;
        for (String[] p : mMarkdown) {
            i++;
            data.setValue("tabs." + i + ".id", p[0]);
            data.setValue("tabs." + i + ".title", p[2]);
            String outFile = FilenameUtils.getName(p[1]).replaceAll(".md", ".html").toLowerCase();
            data.setValue("tabs." + i + ".link", outFile);
        }

        i++;
        data.setValue("tabs." + i + ".id", "java");
        data.setValue("tabs." + i + ".title", "Java API Reference");
        data.setValue("tabs." + i + ".link", javadocDir + "packages.html");

        boolean hasModules = false;
        ClassInfo[] classes = Converter.rootClasses();
        for (ClassInfo cl : classes) {
            if (cl.isModule()) {
                hasModules = true;
            }
        }

        if (hasModules) {
            i++;
            data.setValue("tabs." + i + ".id", "mule");
            data.setValue("tabs." + i + ".title", "Mule API Reference");
            data.setValue("tabs." + i + ".link", muleXmlDir + "modules.html");
        }

        return data;
    }

    public static Data makePackageHDF() {
        Data data = makeHDF();
        ClassInfo[] classes = Converter.rootClasses();

        SortedMap<String, PackageInfo> sorted = new TreeMap<String, PackageInfo>();
        for (ClassInfo cl : classes) {
            PackageInfo pkg = cl.containingPackage();
            String name;
            if (pkg == null) {
                name = "";
            } else {
                name = pkg.name();
            }
            sorted.put(name, pkg);
        }

        int i = 0;
        for (String s : sorted.keySet()) {
            PackageInfo pkg = sorted.get(s);

            if (pkg.isHidden()) {
                continue;
            }
            Boolean allHidden = true;
            int pass = 0;
            ClassInfo[] classesToCheck = null;
            while (pass < 5) {
                switch (pass) {
                case 0:
                    classesToCheck = pkg.ordinaryClasses();
                    break;
                case 1:
                    classesToCheck = pkg.enums();
                    break;
                case 2:
                    classesToCheck = pkg.errors();
                    break;
                case 3:
                    classesToCheck = pkg.exceptions();
                    break;
                case 4:
                    classesToCheck = pkg.getInterfaces();
                    break;
                default:
                    System.err.println("Error reading package: " + pkg.name());
                    break;
                }
                for (ClassInfo cl : classesToCheck) {
                    if (!cl.isHidden()) {
                        allHidden = false;
                        break;
                    }
                }
                if (!allHidden) {
                    break;
                }
                pass++;
            }
            if (allHidden) {
                continue;
            }

            data.setValue("reference", "1");
            data.setValue("reference.apilevels", sinceTagger.hasVersions() ? "1" : "0");
            data.setValue("docs.packages." + i + ".name", s);
            data.setValue("docs.packages." + i + ".link", pkg.htmlPage());
            data.setValue("docs.packages." + i + ".since.key", SinceTagger.keyForName(pkg.getSince()));
            data.setValue("docs.packages." + i + ".since.name", pkg.getSince());
            TagInfo.makeHDF(data, "docs.packages." + i + ".shortDescr", pkg.firstSentenceTags());
            i++;
        }

        sinceTagger.writeVersionNames(data);
        return data;
    }

    private static void writeDirectory(Map<String, List<TocInfo>> toc, File dir, String relative, JSilver js,
            String out) {
        File[] files = dir.listFiles();
        int i, count = files.length;
        for (i = 0; i < count; i++) {
            File f = files[i];
            if (f.isFile()) {
                String templ = ensureSlash(relative) + f.getName();
                int len = templ.length();
                /* if (len > 3 && ".cs".equals(templ.substring(len - 3))) {
                Data data = makeHDF();
                makeTocHDF(toc, data);
                String filename = out + templ.substring(0, len - 3) + htmlExtension;
                ClearPage.write(data, templ, filename, js); */
                if (len > 3 && ".jd".equals(templ.substring(len - 3))) {
                    String filename = out + templ.substring(0, len - 3) + htmlExtension;
                    DocFile.writePage(toc, f.getAbsolutePath(), relative, filename);
                } else {
                    ClearPage.copyFile(f, new File(ensureSlash(out) + templ));
                }
            } else if (f.isDirectory()) {
                writeDirectory(toc, f, ensureSlash(relative) + f.getName() + "/", js, out);
            }
        }
    }

    public static void writeAssets() {
        JarFile thisJar = JarUtils.jarForClass(Doclava.class, null);
        if (thisJar != null) {
            try {
                List<String> templateDirs = ClearPage.getBundledTemplateDirs();
                for (String templateDir : templateDirs) {
                    String assetsDir = ensureSlash(templateDir) + "assets";
                    JarUtils.copyResourcesToDirectory(thisJar, assetsDir,
                            ensureSlash(ClearPage.outputDir) + assetsOutputDir);
                }

                // write mule-developer-core.css
                Data data = makeHDF();
                ClearPage.write(data, "mule-developer-core.cs", assetsOutputDir + "/mule-developer-core.css");
            } catch (IOException e) {
                System.err.println("Error copying assets directory.");
                e.printStackTrace();
                return;
            }
        }

        List<String> templateDirs = ClearPage.getTemplateDirs();
        for (String templateDir : templateDirs) {
            File assets = new File(ensureSlash(templateDir) + "assets");
            if (assets.isDirectory()) {
                writeDirectory(null, assets, assetsOutputDir, null, "");
            }
        }
    }

    public static void writeLists() {
        Data data = makeHDF();

        ClassInfo[] classes = Converter.rootClasses();

        SortedMap<String, Object> sorted = new TreeMap<String, Object>();
        for (ClassInfo cl : classes) {
            if (cl.isHidden()) {
                continue;
            }
            sorted.put(cl.qualifiedName(), cl);
            PackageInfo pkg = cl.containingPackage();
            String name;
            if (pkg == null) {
                name = "";
            } else {
                name = pkg.name();
            }
            sorted.put(name, pkg);
            for (MethodInfo method : cl.methods()) {
                if (method.isProcessor() || method.isSource() || method.isTransformer()) {
                    sorted.put(method.elementName(), method);
                }
            }
        }

        int i = 0;
        for (String s : sorted.keySet()) {
            data.setValue("docs.pages." + i + ".id", "" + i);
            data.setValue("docs.pages." + i + ".label", s);

            Object o = sorted.get(s);
            if (o instanceof PackageInfo) {
                PackageInfo pkg = (PackageInfo) o;
                data.setValue("docs.pages." + i + ".link", "java/" + pkg.htmlPage());
                data.setValue("docs.pages." + i + ".type", "package");
            } else if (o instanceof ClassInfo) {
                ClassInfo cl = (ClassInfo) o;
                data.setValue("docs.pages." + i + ".link", "java/" + cl.htmlPage());
                data.setValue("docs.pages." + i + ".type", "class");
            } else if (o instanceof MethodInfo) {
                MethodInfo mi = (MethodInfo) o;
                data.setValue("docs.pages." + i + ".id", "" + i);
                data.setValue("docs.pages." + i + ".link", "mule/" + mi.relativeModulePath());
                data.setValue("docs.pages." + i + ".type", "method");
            }
            i++;
        }

        ClearPage.write(data, "lists.cs", javadocDir + "lists.js");
    }

    public static void cantStripThis(ClassInfo cl, HashSet<ClassInfo> notStrippable) {
        if (!notStrippable.add(cl)) {
            // slight optimization: if it already contains cl, it already contains
            // all of cl's parents
            return;
        }
        ClassInfo supr = cl.superclass();
        if (supr != null) {
            cantStripThis(supr, notStrippable);
        }
        for (ClassInfo iface : cl.getInterfaces()) {
            cantStripThis(iface, notStrippable);
        }
    }

    private static String getPrintableName(ClassInfo cl) {
        ClassInfo containingClass = cl.containingClass();
        if (containingClass != null) {
            // This is an inner class.
            String baseName = cl.name();
            baseName = baseName.substring(baseName.lastIndexOf('.') + 1);
            return getPrintableName(containingClass) + '$' + baseName;
        }
        return cl.qualifiedName();
    }

    /**
     * Writes the list of classes that must be present in order to provide the non-hidden APIs known
     * to javadoc.
     *
     * @param filename the path to the file to write the list to
     */
    public static void writeKeepList(String filename) {
        HashSet<ClassInfo> notStrippable = new HashSet<ClassInfo>();
        ClassInfo[] all = Converter.allClasses();
        Arrays.sort(all); // just to make the file a little more readable

        // If a class is public and not hidden, then it and everything it derives
        // from cannot be stripped. Otherwise we can strip it.
        for (ClassInfo cl : all) {
            if (cl.isPublic() && !cl.isHidden()) {
                cantStripThis(cl, notStrippable);
            }
        }
        PrintStream stream = null;
        try {
            stream = new PrintStream(filename);
            for (ClassInfo cl : notStrippable) {
                stream.println(getPrintableName(cl));
            }
        } catch (FileNotFoundException e) {
            System.err.println("error writing file: " + filename);
        } finally {
            if (stream != null) {
                stream.close();
            }
        }
    }

    private static PackageInfo[] sVisiblePackages = null;

    public static PackageInfo[] choosePackages() {
        if (sVisiblePackages != null) {
            return sVisiblePackages;
        }

        ClassInfo[] classes = Converter.rootClasses();
        SortedMap<String, PackageInfo> sorted = new TreeMap<String, PackageInfo>();
        for (ClassInfo cl : classes) {
            PackageInfo pkg = cl.containingPackage();
            String name;
            if (pkg == null) {
                name = "";
            } else {
                name = pkg.name();
            }
            sorted.put(name, pkg);
        }

        ArrayList<PackageInfo> result = new ArrayList<PackageInfo>();

        for (String s : sorted.keySet()) {
            PackageInfo pkg = sorted.get(s);

            if (pkg.isHidden()) {
                continue;
            }
            Boolean allHidden = true;
            int pass = 0;
            ClassInfo[] classesToCheck = null;
            while (pass < 5) {
                switch (pass) {
                case 0:
                    classesToCheck = pkg.ordinaryClasses();
                    break;
                case 1:
                    classesToCheck = pkg.enums();
                    break;
                case 2:
                    classesToCheck = pkg.errors();
                    break;
                case 3:
                    classesToCheck = pkg.exceptions();
                    break;
                case 4:
                    classesToCheck = pkg.getInterfaces();
                    break;
                default:
                    System.err.println("Error reading package: " + pkg.name());
                    break;
                }
                for (ClassInfo cl : classesToCheck) {
                    if (!cl.isHidden()) {
                        allHidden = false;
                        break;
                    }
                }
                if (!allHidden) {
                    break;
                }
                pass++;
            }
            if (allHidden) {
                continue;
            }

            result.add(pkg);
        }

        sVisiblePackages = result.toArray(new PackageInfo[result.size()]);
        return sVisiblePackages;
    }

    public static PackageInfo[] chooseModulePackages() {
        ClassInfo[] classes = Converter.rootClasses();
        SortedMap<String, PackageInfo> sorted = new TreeMap<String, PackageInfo>();
        for (ClassInfo cl : classes) {
            if (!cl.isModule()) {
                continue;
            }

            PackageInfo pkg = cl.containingPackage();
            String name;
            if (pkg == null) {
                name = "";
            } else {
                name = pkg.name();
            }
            sorted.put(name, pkg);
        }

        ArrayList<PackageInfo> result = new ArrayList<PackageInfo>();

        for (String s : sorted.keySet()) {
            PackageInfo pkg = sorted.get(s);

            result.add(pkg);
        }

        return result.toArray(new PackageInfo[result.size()]);
    }

    public static void writePackages(String filename) {
        Data data = makePackageHDF();

        int i = 0;
        for (PackageInfo pkg : choosePackages()) {
            writePackage(pkg);

            data.setValue("docs.packages." + i + ".name", pkg.name());
            data.setValue("docs.packages." + i + ".link", pkg.htmlPage());
            TagInfo.makeHDF(data, "docs.packages." + i + ".shortDescr", pkg.firstSentenceTags());

            i++;
        }

        setPageTitle(data, "Package Index");

        TagInfo.makeHDF(data, "root.descr", Converter.convertTags(root.inlineTags(), null));

        ClearPage.write(data, "packages.cs", filename);
        ClearPage.write(data, "package-list.cs", javadocDir + "package-list");

        Proofread.writePackages(filename, Converter.convertTags(root.inlineTags(), null));
    }

    public static void writeMarkdowns() {
        MarkdownProcessor markdown = new MarkdownProcessor();
        for (String[] m : mMarkdown) {
            try {
                String mdContent = FileUtils.readFileToString(new File(m[1]));
                String htmlContent = markdown.markdown(mdContent);
                String outFile = FilenameUtils.getName(m[1]).replaceAll(".md", ".html").toLowerCase();
                Data data = makeHDF();

                data.setValue("content", htmlContent);
                data.setValue("section", m[0]);
                ClearPage.write(data, "markdown.cs", outFile);
            } catch (IOException e) {
                System.err.println("Cannot read " + m[1] + " file: " + e.getMessage());
            }
        }
    }

    public static void writeModules(String filename) {
        Data data = makeHDF();

        int i = 0;
        for (PackageInfo pkg : chooseModulePackages()) {

            data.setValue("reference", "1");
            data.setValue("reference.apilevels", sinceTagger.hasVersions() ? "1" : "0");
            data.setValue("docs.packages." + i + ".name", pkg.name());
            makeModuleListHDF(data, "docs.packages." + i + ".modules", pkg.modules());

            for (int j = 0; j < pkg.modules().length; j++) {
                Data classData = makeHDF();
                ClassInfo mod = pkg.modules()[j];
                writeModule(mod, classData);
            }

            i++;
        }

        setPageTitle(data, "Module Index");

        TagInfo.makeHDF(data, "root.descr", Converter.convertTags(root.inlineTags(), null));

        ClearPage.write(data, "modules.cs", filename);

        Proofread.writePackages(filename, Converter.convertTags(root.inlineTags(), null));
    }

    public static void writeModule(ClassInfo cl, Data data) {
        cl.makeHDF(data);

        setPageTitle(data, cl.name());
        ClearPage.write(data, "module.cs", Doclava.muleXmlDir + cl.modulePath());
        ClearPage.write(data, "schema.cs", Doclava.muleXmlDir + cl.moduleSchemaPath());

        //Proofread.writeClass(cl.modulePath(), cl);
    }

    public static void writePackage(PackageInfo pkg) {
        // these this and the description are in the same directory,
        // so it's okay
        Data data = makePackageHDF();

        String name = pkg.name();

        data.setValue("package.name", name);
        data.setValue("package.since.key", SinceTagger.keyForName(pkg.getSince()));
        data.setValue("package.since.name", pkg.getSince());
        data.setValue("package.descr", "...description...");
        pkg.setFederatedReferences(data, "package");

        makeClassListHDF(data, "package.annotations", ClassInfo.sortByName(pkg.getAnnotations()));
        makeClassListHDF(data, "package.interfaces", ClassInfo.sortByName(pkg.getInterfaces()));
        makeClassListHDF(data, "package.classes", ClassInfo.sortByName(pkg.ordinaryClasses()));
        makeClassListHDF(data, "package.modules", ClassInfo.sortByName(pkg.modules()));
        makeClassListHDF(data, "package.enums", ClassInfo.sortByName(pkg.enums()));
        makeClassListHDF(data, "package.exceptions", ClassInfo.sortByName(pkg.exceptions()));
        makeClassListHDF(data, "package.errors", ClassInfo.sortByName(pkg.errors()));

        TagInfo[] shortDescrTags = pkg.firstSentenceTags();
        TagInfo[] longDescrTags = pkg.inlineTags();
        TagInfo.makeHDF(data, "package.shortDescr", shortDescrTags);
        TagInfo.makeHDF(data, "package.descr", longDescrTags);
        data.setValue("package.hasLongDescr", TagInfo.tagsEqual(shortDescrTags, longDescrTags) ? "0" : "1");

        String filename = Doclava.javadocDir + pkg.relativePath();
        setPageTitle(data, name);
        ClearPage.write(data, "package.cs", filename);

        filename = javadocDir + pkg.fullDescriptionFile();
        setPageTitle(data, name + " Details");
        ClearPage.write(data, "package-descr.cs", filename);

        Proofread.writePackage(filename, pkg.inlineTags());
    }

    public static void writeClassLists() {
        int i;
        Data data = makePackageHDF();

        ClassInfo[] classes = PackageInfo.filterHidden(Converter.convertClasses(root.classes()));
        if (classes.length == 0) {
            return;
        }

        Sorter[] sorted = new Sorter[classes.length];
        for (i = 0; i < sorted.length; i++) {
            ClassInfo cl = classes[i];
            String name = cl.name();
            sorted[i] = new Sorter(name, cl);
        }

        Arrays.sort(sorted);

        // make a pass and resolve ones that have the same name
        int firstMatch = 0;
        String lastName = sorted[0].label;
        for (i = 1; i < sorted.length; i++) {
            String s = sorted[i].label;
            if (!lastName.equals(s)) {
                if (firstMatch != i - 1) {
                    // there were duplicates
                    for (int j = firstMatch; j < i; j++) {
                        PackageInfo pkg = ((ClassInfo) sorted[j].data).containingPackage();
                        if (pkg != null) {
                            sorted[j].label = sorted[j].label + " (" + pkg.name() + ")";
                        }
                    }
                }
                firstMatch = i;
                lastName = s;
            }
        }

        // and sort again
        Arrays.sort(sorted);

        for (i = 0; i < sorted.length; i++) {
            String s = sorted[i].label;
            ClassInfo cl = (ClassInfo) sorted[i].data;
            char first = Character.toUpperCase(s.charAt(0));
            cl.makeShortDescrHDF(data, "docs.classes." + first + '.' + i);
        }

        setPageTitle(data, "Class Index");
        ClearPage.write(data, "classes.cs", javadocDir + "classes" + htmlExtension);
    }

    // we use the word keywords because "index" means something else in html land
    // the user only ever sees the word index
    /*
    * public static void writeKeywords() { ArrayList<KeywordEntry> keywords = new
    * ArrayList<KeywordEntry>();
    *
    * ClassInfo[] classes = PackageInfo.filterHidden(Converter.convertClasses(root.classes()));
    *
    * for (ClassInfo cl: classes) { cl.makeKeywordEntries(keywords); }
    *
    * HDF data = makeHDF();
    *
    * Collections.sort(keywords);
    *
    * int i=0; for (KeywordEntry entry: keywords) { String base = "keywords." + entry.firstChar() +
    * "." + i; entry.makeHDF(data, base); i++; }
    *
    * setPageTitle(data, "Index"); ClearPage.write(data, "keywords.cs", javadocDir + "keywords" +
    * htmlExtension); }
    */

    public static void writeHierarchy() {
        ClassInfo[] classes = Converter.rootClasses();
        ArrayList<ClassInfo> info = new ArrayList<ClassInfo>();
        for (ClassInfo cl : classes) {
            if (!cl.isHidden()) {
                info.add(cl);
            }
        }
        Data data = makePackageHDF();
        Hierarchy.makeHierarchy(data, info.toArray(new ClassInfo[info.size()]));
        setPageTitle(data, "Class Hierarchy");
        ClearPage.write(data, "hierarchy.cs", javadocDir + "hierarchy" + htmlExtension);
    }

    public static void writeClasses() {
        ClassInfo[] classes = Converter.rootClasses();
        if (generateSources) {
            mHDFData.add(new String[] { "doclava.generate.sources", "true" });
            for (ClassInfo cl : classes) {
                Data data = makePackageHDF();
                if (!cl.isHidden()) {
                    writeSource(cl, data);
                    writeClass(cl, data);
                }
            }
        } else {
            for (ClassInfo cl : classes) {
                Data data = makePackageHDF();
                if (!cl.isHidden()) {
                    writeClass(cl, data);
                }
            }
        }
    }

    public static void writeClass(ClassInfo cl, Data data) {
        cl.makeHDF(data);

        setPageTitle(data, cl.name());
        ClearPage.write(data, "class.cs", Doclava.javadocDir + cl.relativePath());

        Proofread.writeClass(cl.htmlPage(), cl);
    }

    public static void writeSource(ClassInfo cl, Data data) {
        try {
            cl.makeHDF(data);
            data.setValue("class.source", SampleTagInfo.escapeHtml(cl.getSource()));
            setPageTitle(data, cl.name());
            ClearPage.write(data, "source.cs", Doclava.javadocDir + cl.relativePath("-source"));
        } catch (IOException e) {
            Errors.error(Errors.IO_ERROR, null, "Could not find source file for " + cl.name());
        }
    }

    public static void makeClassListHDF(Data data, String base, ClassInfo[] classes) {
        for (int i = 0; i < classes.length; i++) {
            ClassInfo cl = classes[i];
            if (!cl.isHidden()) {
                cl.makeShortDescrHDF(data, base + "." + i);
            }
        }
    }

    public static void makeModuleListHDF(Data data, String base, ClassInfo[] classes) {
        for (int i = 0; i < classes.length; i++) {
            ClassInfo cl = classes[i];
            if (!cl.isHidden()) {
                cl.makeModuleShortDescrHDF(data, base + "." + i);
            }
        }
    }

    public static String linkTarget(String source, String target) {
        String[] src = source.split("/");
        String[] tgt = target.split("/");

        int srclen = src.length;
        int tgtlen = tgt.length;

        int same = 0;
        while (same < (srclen - 1) && same < (tgtlen - 1) && (src[same].equals(tgt[same]))) {
            same++;
        }

        String s = "";

        int up = srclen - same - 1;
        for (int i = 0; i < up; i++) {
            s += "../";
        }

        int N = tgtlen - 1;
        for (int i = same; i < N; i++) {
            s += tgt[i] + '/';
        }
        s += tgt[tgtlen - 1];

        return s;
    }

    /**
     * Returns true if the given element has an @hide or @pending annotation.
     */
    private static boolean hasHideAnnotation(Doc doc) {
        String comment = doc.getRawCommentText();
        return comment.indexOf("@hide") != -1 || comment.indexOf("@pending") != -1;
    }

    /**
     * Returns true if the given element is hidden.
     */
    private static boolean isHidden(Doc doc) {
        // Methods, fields, constructors.
        if (doc instanceof MemberDoc) {
            return hasHideAnnotation(doc);
        }

        // Classes, interfaces, enums, annotation types.
        if (doc instanceof ClassDoc) {
            ClassDoc classDoc = (ClassDoc) doc;

            // Check the containing package.
            if (hasHideAnnotation(classDoc.containingPackage())) {
                return true;
            }

            // Check the class doc and containing class docs if this is a
            // nested class.
            ClassDoc current = classDoc;
            do {
                if (hasHideAnnotation(current)) {
                    return true;
                }

                current = current.containingClass();
            } while (current != null);
        }

        return false;
    }

    /**
     * Filters out hidden elements.
     */
    private static Object filterHidden(Object o, Class<?> expected) {
        if (o == null) {
            return null;
        }

        Class<?> type = o.getClass();
        if (type.getName().startsWith("com.sun.")) {
            // TODO: Implement interfaces from superclasses, too.
            return Proxy.newProxyInstance(type.getClassLoader(), type.getInterfaces(), new HideHandler(o));
        } else if (o instanceof Object[]) {
            Class<?> componentType = expected.getComponentType();
            Object[] array = (Object[]) o;
            List<Object> list = new ArrayList<Object>(array.length);
            for (Object entry : array) {
                if ((entry instanceof Doc) && isHidden((Doc) entry)) {
                    continue;
                }
                list.add(filterHidden(entry, componentType));
            }
            return list.toArray((Object[]) Array.newInstance(componentType, list.size()));
        } else {
            return o;
        }
    }

    /**
     * Filters hidden elements out of method return values.
     */
    private static class HideHandler implements InvocationHandler {

        private final Object target;

        public HideHandler(Object target) {
            this.target = target;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            if (args != null) {
                if (methodName.equals("compareTo") || methodName.equals("equals") || methodName.equals("overrides")
                        || methodName.equals("subclassOf")) {
                    args[0] = unwrap(args[0]);
                }
            }

            if (methodName.equals("getRawCommentText")) {
                return filterComment((String) method.invoke(target, args));
            }

            // escape "&" in disjunctive types.
            if (proxy instanceof Type && methodName.equals("toString")) {
                return ((String) method.invoke(target, args)).replace("&", "&amp;");
            }

            try {
                return filterHidden(method.invoke(target, args), method.getReturnType());
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
        }

        private String filterComment(String s) {
            if (s == null) {
                return null;
            }

            s = s.trim();

            // Work around off by one error
            while (s.length() >= 5 && s.charAt(s.length() - 5) == '{') {
                s += "&nbsp;";
            }

            return s;
        }

        private static Object unwrap(Object proxy) {
            if (proxy instanceof Proxy) {
                return ((HideHandler) Proxy.getInvocationHandler(proxy)).target;
            }
            return proxy;
        }
    }

    /**
     * Ensures a trailing '/' at the end of a string.
     */
    static String ensureSlash(String path) {
        return path.endsWith("/") ? path : path + "/";
    }
}