Doclet your servlet!

Write better documentation with ServletDoclet

Page 2 of 3

Writing the HTML documentation

The first step in writing HTML documentation is to create a PrintStream that writes to a file. In this case, the filename is derived from the servlet name, with the suffix "Info.html" added by default. If the user designated a folder on the command line, the file is created in that folder; otherwise, it is created in the working directory. The code that creates the PrintStream appears below:

static PrintWriter makePrintStream(ServletDoc doc) 
                           throws IOException {
    return new PrintWriter(
                      new FileOutputStream(makeFile(doc)));
}
     
static File makeFile(ServletDoc doc) {
    String filename = doc.name() + sFilenameSuffix;
    if (sFolder != null && sFolder.exists() && sFolder.isDirectory()) {
        return new File(sFolder,filename); 
    }
         
    if (sFolder != null) {
        warn("warning: folder does not exist " + sFolder);
    }
         
    return new File(filename); 
}

For each servlet, ServletDoclet creates a PrintStream and then passes the ServletDoc and the PrintStream to a HtmlDocletWriter, which actually writes out the content. By separating the servlet documentation data from the servlet documentation formatting, you make it easier to change the formatting. As described later, that can be done via a command-line option that specifies an alternative class to HtmlDocletWriter.

The write() method of HtmlDocletWriter goes through all the steps of creating a well-formed HTML document:

public void write(ServletDoc doc, PrintWriter out) {
    writeHtmlHeader(doc, out);
    writeHtmlBodyHeader(doc,out);
    writeOverview(doc, out);
    
    
    Enumeration controllers = doc.controllers();
    while (controllers.hasMoreElements()) {
        writeController((ControllerDoc)controllers.nextElement(),
                         doc, 
                         out);
    }
   
    writeHtmlBodyFooter(out);
    writeHtmlFooter(out);             
}

Of this code, the writeHtmlHeader(), writeHtmlBodyHeader(), writeHtmlBodyFooter(), and writeHtmlFooter() methods are pretty obvious. They consist of statements such as:

    out.println("<html>");

The interesting methods write the servlet overview and each controller. Since the overview is simpler, let's start with that method. The writeOverview() method begins by writing a level-one header containing the servlet name (from the ServletDoc's name() method), which will tell the user what servlet the documentation is for. Then the writeOverview() method creates a paragraph containing the description of the servlet. As you can see, writing HTML documentation is pretty easy once you have the right information in a Doc class.

protected void writeOverview(ServletDoc doc, PrintWriter out) {
    out.println("<h1>" + doc.name() + " Documentation</h1>");
    out.println("<p>");
    out.println(doc.description());
    out.println("</p>"); 
}

Writing the documentation for a controller is more involved but not really any more difficult. The writeController() method lists the steps:

protected void writeController( ControllerDoc cdoc, 
                                ServletDoc doc, 
                                PrintWriter out) {
    writeControllerOverview(cdoc, doc, out);
    writeParameters(cdoc, doc, out);
    writeBeans(cdoc, doc, out);
    writeJsps(cdoc, doc, out);   
}

In the overview section, you write out the controller's URL and its description, followed by samples of how to use it in a hyperlink or form. You can directly copy those usage samples into an HTML or JSP Webpage, and edit them to specify the parameters. To write the usage sample, you must know the URL that invokes the servlet as well as the parameters that can be used with it. The ControllerDoc provides that information in its URL and requestParameter() methods, respectively. In the case of a hyperlink, the code below iterates through the parameters and builds up a URL, adding a &param=value for each parameter. In the case of a form, the code iterates through the parameters and adds an <input> for each parameter, with the type of input and value to be specified by the user. Note that because you are outputting sample HTML code that you want to appear literally in the Webpage, you must use "&lt;" and "&gt;" instead of "<" and ">". Likewise, the funny construction "&amp;amp;" will appear in the Webpage as "&amp;".

protected void writeControllerOverview(ControllerDoc cdoc, 
                                       ServletDoc doc, 
                                       PrintWriter out) {
    String url = cdoc.url();
    Tag[] paramTag = cdoc.requestParameters();
    out.println("<hr>");
    out.println("<h2>URL: " + url + "</h2>");
    out.println("<p>" + cdoc.description() + "</p>");
    out.println("<h3>Sample Usage</h3><p>");
    out.print("<a href=\"");
    out.print(url);
    if (paramTag.length > 0) {
        out.print("?");
        for(int i = 0; i < paramTag.length; ++i) {
            if (i > 0) out.print("&amp;");
                out.print(wordFromString(paramTag[i].text(),1));
                out.print("=");
                out.print("value");
                out.print(i + 1);
        }
    }
    out.println("\">link</a>");
    out.println("<br><br>");
    out.println("<form method=\"post\" action=\"" + 
                    url + "\"><br>");
    for(int i = 0; i < paramTag.length; ++i) {
        out.println("<input name=\"" + 
            wordFromString(paramTag[i].text(),1) +
                "\" type=\"\" value=\"\"><br>");
      }
    out.println("<input type=\"submit\" name=\"submit\" " +
                  value=\"submit\"><br>");
    out.println("</form>");
    out.println("</p>"); 
}

The wordFromString() method here extracts a particular word ("1" is the first word) from the tag. Recall that the first word of a requestParameter tag is the name of the parameter and that the remaining words are comment. The remainderOfString() method could be used to retrieve the comment without the parameter name (although that information is not needed here). Those two methods are available in the source code and use a StringTokenizer to do their work.

Now I'll skip to the writeBeans() method, which writes out the information about the JavaBeans made available to the JSP as either request attributes or session beans. For each bean, HtmlDocletWriter writes out a jsp:useBean tag that illustrates its use in a JSP. It also writes out a comment describing the bean. Since the handling of request and session beans are mostly the same, most of the work is delegated to the writeBeanDetail() method with the scope of the bean (request or session) as a parameter. Again the wordFromString() method is used to extract the two first parts of the comment for a bean, the bean's type and name. The remainderOfString() method is used to extract the rest of the comment, which is, in fact, the proper comment.

protected void writeBeans(ControllerDoc cdoc, 
    ServletDoc doc, PrintWriter out) {
    out.println("<h3>Beans</h3>");
    Tag[] requestBean = cdoc.requestBeans();
    for(int i = 0; i < requestBean.length; ++i) {
            writeBeanDetail(requestBean[i],"request",doc, out);
    }
    
    Tag[] sessionBean = cdoc.sessionBeans();
    for(int i = 0; i < sessionBean.length; ++i) {
        writeBeanDetail(sessionBean[i],"session", doc, out);
    }
}
protected void writeBeanDetail( Tag beanTag, 
                                String scope, 
                                ServletDoc doc, 
                                PrintWriter out) {
    String   text = beanTag.text(),
             beanType = wordFromString(text,1),
             beanName = wordFromString(text,2),
             description = remainderOfString(text,2),
             link = getBeanLink(beanType, doc);
    out.println("<p>");
    writeUseBean(beanName, beanType, scope, link, out);
    out.println("<br>");
    out.println(description);
    out.println("</p>");
}
protected void writeUseBean(    String name, 
                                String type, 
                                String scope, 
                                String link, 
                                PrintWriter out) {
    out.println("<code>");
    out.print("<jsp:useBean id=\"" + name + 
                "\" scope=\"" + scope +"\" class=\"");
    if (link != null) {
         out.print("<a href=\"" + link + "\">");
         out.print(type);
         out.print("</a>");
    }
    else {
         out.print(type);
    }
    out.print("\"/>");
    out.println("</code>");
}

I will not go into detail on the other methods that write out parts of the documentation for a controller; they are very similar to the above methods.

Linking to external JavaDoc

The writeUseBean() method accomplishes a neat trick, which deserves further discussion. The trick is constructing a hyperlink in the documentation from the class of a bean to the regular JavaDoc documentation for that class. That is useful because when you have a jsp:useBean tag in a JSP, you typically want to write additional tags that access or set that bean's properties. You could, with much effort, extend your doclet to also document such beans. Fortunately, there is an easier way: You can link from your doclet-based documentation to standard JavaDoc documentation.

JavaDoc makes a provision for such linking in its -link option. That option is followed by a URL to the root of an external documentation directory. In that directory, there is (by convention) a file called package-list, with one package name per line. The package-list lists the packages that are documented within the external directory. Using the package-list, you can determine whether a particular class is documented in an external directory and, thus, whether a link is possible. Since multiple -link options can be provided, each for a different external documentation directory, the package-list helps you find the right baseURL for a given package and class.

Unfortunately, JavaDoc does not process the -link option for a doclet; you have to do all the work yourself. That work begins in the readOptions() method, which reads and acts upon command-line options. ServletDoclet supports three options: a default directory into which you can write the documentation, a class to be used as the writer (in place of HtmlDocletWriter), and the link option. The readOptions() method is passed a two-dimensional array of strings. The first dimension has one array per command-line option. The second dimension has all the words that were part of a given option, starting with the name of the option. Hence, the method below iterates through the options, checking to see if the first word of the option is a known option type. If it is a known option type, the method dispatches on the option type, using the second word as a parameter to the option. In the case of -link, the second word is the URL, and you call the addLink() method to store it. The addLink() method makes an instance of ExternalLink with the given URL and stores it in a Vector link. I will discuss ExternalLink next.

private static void readOptions(String[][] options) {
    for (int i = 0; i < options.length; i++) {
        String[] opt = options[i];
        if (opt[0].equals("-d")) {
            sFolder = new File(opt[1]);
        }
        else if (opt[0].equals("-link")) {
            addLink(opt[1]);
        }
        else if (opt[0].equals("-writer")) {
            sDocletWriterClass = opt[1];
        }
    }
}
static void addLink(String link) {
    try {
        sExternalLinks.addElement(
                  new ExternalLink(new URL(link)));
  
    }
    catch (java.net.MalformedURLException ex) {
 
    }
 }

The ExternalLink class provides two essential methods. First, its containsPackage() method determines if that link contains a particular package. Second, if containsPackage() returns true, makeExternalDocURL() can be called with a package and class to create a URL that links to the documentation for that class. Upon initialization of ExternalLink, you read the list of packages from the package-list file by constructing a URL to the package-list, opening a stream to it, and reading each line with a BufferedReader. You store the list of packages in a Vector, which has the satisfying consequence of making the containsPackage() method implementation a one-liner.

public ExternalLink(URL url) {
    mBaseURL = url;
    mPackageList = new Vector();
    initialize();
}
private void initialize() {
    // try/catch/finally omitted for readability
    BufferedReader reader = null;
    InputStream stream = null;
    
    URL packageListURL = new URL(mBaseURL,"package-list");
    stream = packageListURL.openStream();
    reader = new BufferedReader(new InputStreamReader(stream));
    for(    String thePackage = reader.readLine(); 
           thePackage != null; 
           thePackage = reader.readLine()) {
        mPackageList.addElement(thePackage);
    }
}
public boolean containsPackage(String packageName) {
    return mPackageList.contains(packageName); 
}

Constructing a URL that points to external documentation requires extending the base URL with a path consisting of all the directories in the class's package name, followed by the class name and the .html extension.

public String makeExternalDocURL(String packageName, String className) {
    StringBuffer buffer = new StringBuffer();
    buffer.append(mBaseURL.toString());
    buffer.append(packageName.replace('.', slash()));
    buffer.append(slash());
    buffer.append(className);
    buffer.append(".html");
    return buffer.toString();
}
| 1 2 3 Page 2