JavaDoc is good for documenting Java code. But sometimes it is better to have tables than plain text. Of course you could code the HTML code for tables manually, but it is more comfortable if JavaDoc would support that ‚requirement‘ directly – which it does not. But you can extend JavaDoc by writing a Taglet.
We want to achieve that your JavaDoc comment
/**
@my.tag
#column1 Text for column 1
#column2 Text for column 2
*/
produces the required HTML.
JavaDoc uses Doclets as extension point. Writing a doclet is possible but too much overhead because its responsibility is to do the whole generation, but we only want to add a new feature. By default the StandardDoclet is used. And that provides an additional extension point: the taglet. With that you can write new JavaDoc tags (like the @my.tag) and specify the generation behaviour. Using Taglets consist on these steps:
- writing an implementation of com.sun.tools.doclets.Taglet
- add the taglet to the generation process, e.g. by Ant’s <javadoc><taglet name=classname path=pathOfJar>.
When you implement the Taglet interface you have to implement these methods
public String getName() {
return NAME;
}
public boolean inField() {
return false;
}
public boolean inConstructor() {
return false;
}
public boolean inMethod() {
return true;
}
public boolean inOverview() {
return false;
}
public boolean inPackage() {
return false;
}
public boolean inType() {
return false;
}
public boolean isInlineTag() {
return false;
}
public static void register(Map registeredTaglets);
public String toString(Tag tag);
public String toString(Tag[] tags);
getName() returns the name of the taglet which should be the name used after the @ sign. I used a constant NAME=“my.tag“.
The inXXX and isXXX methods specify the kind of the taglet. Because I want to have the tables only in method documentation this taglet is not valid for fields and constructors, types (means the class itself) or package, but for methods. Its content shouldnt be on the overview page (only on the detail page). This taglet shouldnt define an inline tag (like in ‚text text {@link target} text text‘).
The next three methods are the more interesting …
The register() method is a static method (and therefore not defined by the interface) and responsible for registering the taglet by the StandardDoclet. If you specify the -taglet option or use the <taglet> instruction in Ant that method is invoked. The StandardDoclet passes a Map with all registered taglets. So do these steps
- remove the (old) registration if present
- add the new registration
public static void register(Map registeredTaglets) {
MyTaglet tag = new MyTaglet();
Taglet t = (Taglet) registeredTaglets.get(tag.getName());
if (t != null) {
registeredTaglets.remove(tag.getName());
}
registeredTaglets.put(tag.getName(), tag);
}
The two toString() methods are responsible for the conversion from the taglet text to the HTML code. The first gets one taglet comment, the second gets multiple comments. So the easiest implementation for the ‚one‘ method is delegating to the ‚multiple‘ method.
public String toString(Tag tag) {
return toString(new Tag[]{tag});
}
So the last thing to do is implementing the conversion of the tag-array:
- print the tag name as header
- indent the output as all other (standard) tags do
- print the table header
- for each tag print the row
- close opened HTML tags
public String toString(Tag[] tags) {
// Quick exit
if (tags == null || tags.length == 0) {
return null;
}
// HTML collector
StringBuffer sb = new StringBuffer();
// 1.Header
sb.append("<dt><b>").append(getName()).append("</b></dt>").append(BR);
// 2.Indent
sb.append("<dd>").append(BR);
// 3.table header
sb.append("
<table border='1'>").append(BR);
sb.append("
<tr>").append(BR);
sb.append("
<th>column 1</th>
").append(BR);
sb.append("
<th>column 2</th>
").append(BR);
sb.append("</tr>
").append(BR);
// 4. for each tag make a row
String[] stopwords = new String[]{"column1", "column2"};
for (int i = 0; i < tags.length; i++) {
appendTagAsHtml(sb, new TagInfo(tags[i].text(), stopwords));
}
// 5.close all opened tags
sb.append("</table>
").append(BR);
sb.append("</dd>").append(BR);
// return the HTML coding
return sb.toString();
}
In this code I use two helpers: the method appendTagAsHtml() and the class TagInfo.
The TagInfo class constructor takes the text for the tag (all after the @tagname until the next @tag or end of javadoc) and the supported stopwords. It creates a slot for each stopword and one for the text before the first stopword. Then it parses the text and stores the words in the current slot. It starts with the before-slot and goes to the next slot if a stopword occurs.
public class TagInfo {
HashMap<String,List<String>> map = new HashMap();
TagInfo(String tagText, String[] stopwords) {
map.put("#__text__", new ArrayList());
for (int i = 0; i < stopwords.length; i++) {
map.put("#" + stopwords[i], new ArrayList());
}
String currentSlot = "#__text__";
StringTokenizer tokenizer = new StringTokenizer(tagText);
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (map.containsKey(token)) {
currentSlot = token;
} else {
((List)map.get(currentSlot)).add(token);
}
}
}
public List get(String stopword) {
return (List)map.get("#" + stopword);
}
}
[/sourcecode]
The appendInfoAlsHtml() method converts the TagInfo to HTML. It uses another helper method for converting lists to strings.
[sourcecode language='java']
private static void appendInfoAlsHtml2(StringBuffer sb, PruefungsInfo info) {
sb.append("
<tr valign='top'>").append(BR);
sb.append("
<td>")
.append(list2stringbuffer(info.get("column1"), " "))
.append("</td>
")
.append(BR);
sb.append("
<td>")
.append(list2stringbuffer(info.get("column2"), " "))
.append("</td>
")
.append(BR);
.append("</td>
")
.append(BR);
.append(BR);
sb.append("</tr>
").append(BR);
}
protected static StringBuffer list2stringbuffer(List list, String separator) {
StringBuffer rv = new StringBuffer();
for (int i=0; i
<list.size()-1; i++) {
rv.append(list.get(i));
rv.append(separator);
}
if (list.size() > 0) {
// 'get' is 0-based but 'size' is 1-based
rv.append(list.get(list.size()-1));
}
return rv;
}
Finally the use of the taglet in Ant is this:
<target name="javadoc">
<mkdir dir="${javadoc.dir}"/>
<javadoc sourcepath="${java.src}"
destdir="${javadoc.dir}"
author="${javadoc.author}"
version="${javadoc.version}"
use="${javadoc.use}"
access="${javadoc.access}"
excludepackagenames="${javadoc.excludes}"
windowtitle="${ant.project.name} - API">
<doctitle><![CDATA[
<h1>${ant.project.name} - API</h1>
]]></doctitle>
<bottom><![CDATA[<i>Copyright © 2002-${tstamp.jahr} ACME All Rights Reserved.</i>]]></bottom>
<taglet name="pack.age.of.MyTaglet" path="${taglet.jar}"/>
</javadoc>
</target>
p.s.
Sorry for unintentional linebreaks in the coding. Maybe my WordPress knowledge is not big enough …