// The five files // Option.java // OptionGroup.java // Options.java // Unpublicized.java // OptionsDoclet.java // together comprise the implementation of command-line processing. package plume; import java.io.*; import java.util.*; import java.lang.reflect.*; import com.sun.javadoc.*; import java.lang.Class; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringEscapeUtils; /** * Generates HTML documentation of command-line options. * <p> * * <b>Usage</b> <p> * This doclet is typically invoked with: * <pre>javadoc -quiet -doclet plume.OptionsDoclet [doclet options] [java files]</pre> * <p> * * <b>Doclet Options</b> <p> * The following doclet options are supported: * <ul> * <li> <b>-docfile</b> <i>file</i> When specified, the output of this doclet * is the result of replacing everything between the two lines * <pre><!-- start options doc (DO NOT EDIT BY HAND) --></pre> * and * <pre><!-- end options doc --></pre> * in <i>file</i> with the options documentation. This can be used for * inserting option documentation into an existing manual. The existing * docfile is not modified; output goes to the <code>-outfile</code> * argument, or to standard out. * * <li> <b>-outfile</b> <i>file</i> The destination for the output (the default * is standard out). If both <code>-outfile</code> and <code>-docfile</code> * are specified, they must be different. * * <li> <b>-i</b> Specifies that the docfile should be edited in-place. This * option can not be used at the same time as the <code>-outfile</code> option. * * <li> <b>-format</b> <i>format</i> This option sets the output format of this * doclet. Currently, the following values for <i>format</i> are supported: * <ul> * <li> <b>javadoc</b> When this format is specified, the output of this * doclet is formatted as a Javadoc comment. This is useful for including * option documentation inside Java source code. When this format is used * with the <code>-docfile</code> option, the generated documentation is * inserted between the lines * <pre>* <!-- start options doc (DO NOT EDIT BY HAND) --></pre> * and * <pre>* <!-- end options doc --></pre> * using the same indentation. Inline <code>@link</code> and * <code>@see</code> tags in the Javadoc input are left untouched. * * <li> <b>html</b> This format outputs HTML for general purpose use, meaning * inline <code>@link</code> and <code>@see</code> tags in the Javadoc input * are suitably replaced. This is the default output format and does not * need to be specified explicitly. * </ul> * * <li> <b>-classdoc</b> When specified, the output of this doclet includes the * class documentation of the first class specified on the command-line. * * <li> <b>-singledash</b> When specified, <code>use_single_dash(true)</code> is * called on the underlying instance of Options used to generate documentation. * See {@link plume.Options#use_single_dash(boolean)}. * </ul> * <p> * * <b>Examples</b> <p> * To update the Javarifier HTML manual with option documentation run: * <pre>javadoc -quiet -doclet plume.OptionsDoclet -i -docfile javarifier.html src/javarifier/Main.java</pre> * <p> * * To update the class Javadoc for plume.Lookup with option documentation run: * <pre>javadoc -quiet -doclet plume.OptionsDoclet -i -docfile Lookup.java -format javadoc Lookup.java</pre> * <p> * * <b>Requirements</b> <p> * Classes passed to OptionsDoclet that have <code>@Option</code> annotations on * non-static fields should have a nullary (no-argument) constructor. The * nullary constructor may be private or public. This is required because an * object instance is needed to get the default value of a non-static field. It * is cleaner to require a nullary constructor instead of trying to guess * arguments to pass to another constructor. <p> * * <b>Hiding default value strings</b> <p> * By default, the documentation generated by OptionsDoclet includes a default * value string for each option in square brackets after the option's * description, similar to the usage messages generated by {@link * plume.Options#usage(String...)}. The {@link plume.Option#noDocDefault} * field in the <code>@Option</code> annotation can be set to <code>true</code> * to omit the default value string from the generated documentation for that * option. <p> * * Omitting the generated default value string is useful for options that have * system dependent defaults. Such options are not an issue for usage messages * that are generated at runtime. However, system dependent defaults do pose * a problem for static documentation, which is rarely regenerated and meant to * apply to all users. Consider the following <code>@Option</code>-annotated * field: * <pre> * @Option(value="<timezone> Set the time zone") * public static String timezone = TimeZone.getDefault().getID();</pre> * The default value for <code>timezone</code> depends on the system's timezone * setting. HTML documentation of this option generated in Chicago would not * apply to a user in New York. To work around this problem, the * default value should be hidden; instead the Javadoc for this field * should indicate a special default as follows. * <pre> * /** * * <other stuff...> This option defaults to the system timezone. * */ * @Option(value="<timezone> Set the timezone", noDocDefault=true) * public static String timezone = TimeZone.getDefault().getID();</pre> * This keeps the documentation system-agnostic. <p> * * <b>Caveats</b> <p> * The generated HTML documentation includes unpublicized option groups but not * <code>@Unpublicized</code> options. Option groups which contain only * <code>@Unpublicized</code> options are not included in the output at all. <p> * * <b>Troubleshooting</b> <p> * If you get an error such as <tt>ARGH! @Option</tt>, then you are using a * buggy version of gjdoc, the GNU Classpath implementation of Javadoc. * To avoid the problem, upgrade or use a different Javadoc implementation. <p> * * @see plume.Option * @see plume.Options * @see plume.OptionGroup * @see plume.Unpublicized */ // This doesn't itself use plume.Options for its command-line option // processing because a Doclet is required to implement the optionLength // and validOptions methods. public class OptionsDoclet { @SuppressWarnings("nullness") // line.separator property always exists private static String eol = System.getProperty("line.separator"); private static String usage = "Provided by Options doclet:\n" + "-docfile <file> Specify file into which options documentation is inserted\n" + "-outfile <file> Specify destination for resulting output\n" + "-i Edit the docfile in-place\n" + "-format javadoc Format output as a Javadoc comment\n" + "-classdoc Include 'main' class documentation in output\n" + "-singledash Use single dashes for long options (see plume.Options)\n" + "See the OptionsDoclet documentation for more details."; private static String list_help = "<tt>[+]</tt> marked option can be specified multiple times"; private String startDelim = "<!-- start options doc (DO NOT EDIT BY HAND) -->"; private String endDelim = "<!-- end options doc -->"; private File docFile; private File outFile; private boolean inPlace = false; private boolean formatJavadoc = false; private boolean includeClassDoc = false; private RootDoc root; private Options options; public OptionsDoclet(RootDoc root, Options options) { this.root = root; this.options = options; } // Doclet-specific methods /** * Entry point for the doclet. */ public static boolean start(RootDoc root) { List<Object> objs = new ArrayList<Object>(); for (ClassDoc doc : root.specifiedClasses()) { // TODO: Class.forName() expects a binary name but doc.qualifiedName() // returns a fully qualified name. I do not know a good way to convert // between these two name formats. For now, we simply ignore inner // classes. This limitation can be removed when we figure out a better // way to go from ClassDoc to Class<?>. if (doc.containingClass() != null) continue; Class<?> clazz; try { clazz = Class.forName(doc.qualifiedName(), true, Thread.currentThread().getContextClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); return false; } if (needsInstantiation(clazz)) { try { Constructor<?> c = clazz.getDeclaredConstructor(); c.setAccessible(true); objs.add(c.newInstance()); } catch (Exception e) { e.printStackTrace(); return false; } } else { objs.add(clazz); } } Object[] objarray = objs.toArray(); Options options = new Options(objarray); if (options.getOptions().size() < 1) { System.out.println("Error: no @Option-annotated fields found"); return false; } OptionsDoclet o = new OptionsDoclet(root, options); o.setOptions(root.options()); o.processJavadoc(); try { o.write(); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * Returns the number of tokens corresponding to a command-line argument of * this doclet or 0 if the argument is unrecognized. This method is * automatically invoked. * * @see <a href="http://java.sun.com/javase/6/docs/technotes/guides/javadoc/doclet/overview.html">Doclet overview</a> */ public static int optionLength(String option) { if (option.equals("-help")) { System.out.println(usage); return 1; } if (option.equals("-i") || option.equals("-classdoc") || option.equals("-singledash")) { return 1; } if (option.equals("-docfile") || option.equals("-outfile") || option.equals("-format")) { return 2; } return 0; } /** * Tests the validity of command-line arguments passed to this doclet. * Returns true if the option usage is valid, and false otherwise. This * method is automatically invoked. * * @see <a href="http://java.sun.com/javase/6/docs/technotes/guides/javadoc/doclet/overview.html">Doclet overview</a> */ public static boolean validOptions(String options[][], DocErrorReporter reporter) { boolean hasDocFile = false; boolean hasOutFile = false; boolean hasFormat = false; boolean inPlace = false; String docFile = null; String outFile = null; for (int oi = 0; oi < options.length; oi++) { String[] os = options[oi]; String opt = os[0].toLowerCase(); if (opt.equals("-docfile")) { if (hasDocFile) { reporter.printError("-docfile option specified twice"); return false; } File f = new File(os[1]); if (!f.exists()) { reporter.printError("file not found: " + os[1]); return false; } docFile = os[1]; hasDocFile = true; } if (opt.equals("-outfile")) { if (hasOutFile) { reporter.printError("-outfile option specified twice"); return false; } if (inPlace) { reporter.printError("-i and -outfile can not be used at the same time"); return false; } outFile = os[1]; hasOutFile = true; } if (opt.equals("-i")) { if (hasOutFile) { reporter.printError("-i and -outfile can not be used at the same time"); return false; } inPlace = true; } if (opt.equals("-format")) { if (hasFormat) { reporter.printError("-format option specified twice"); return false; } if (!os[1].equals("javadoc") && !os[1].equals("html")) { reporter.printError("unrecognized output format: " + os[1]); return false; } hasFormat = true; } } if (docFile != null && outFile != null && outFile.equals(docFile)) { reporter.printError("docfile must be different from outfile"); return false; } return true; } /** * Set the options for this class based on command-line arguments given by * RootDoc.options(). */ public void setOptions(String[][] options) { for (int oi = 0; oi < options.length; oi++) { String[] os = options[oi]; String opt = os[0].toLowerCase(); if (opt.equals("-docfile")) { this.docFile = new File(os[1]); } else if (opt.equals("-outfile")) { this.outFile = new File(os[1]); } else if (opt.equals("-i")) { this.inPlace = true; } else if (opt.equals("-format")) { if (os[1].equals("javadoc")) setFormatJavadoc(true); } else if (opt.equals("-classdoc")) { this.includeClassDoc = true; } else if (opt.equals("-singledash")) { setUseSingleDash(true); } } } /** * Determine if a class needs to be instantiated in order to work properly * with {@link Options}. */ private static boolean needsInstantiation(Class<?> clazz) { for (Field f : clazz.getDeclaredFields()) { if (f.isAnnotationPresent(Option.class) && !Modifier.isStatic(f.getModifiers())) return true; } return false; } // File IO methods /** * Write the output of this doclet to the correct file. */ public void write() throws Exception { PrintWriter out; String output = output(); if (outFile != null) out = new PrintWriter(new BufferedWriter(new FileWriter(outFile))); else if (inPlace) out = new PrintWriter(new BufferedWriter(new FileWriter(docFile))); else out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out))); out.println(output); out.flush(); out.close(); } /** * Get the final output of this doclet. The string returned by this method * is the output seen by the user. */ public String output() throws Exception { if (docFile == null) { if (formatJavadoc) return optionsToJavadoc(0); else return optionsToHtml(); } return newDocFileText(); } /** * Get the result of inserting the options documentation into the docfile. */ private String newDocFileText() throws Exception { StringBuilderDelimited b = new StringBuilderDelimited(eol); BufferedReader doc = new BufferedReader(new FileReader(docFile)); String docline; boolean replacing = false; boolean replaced_once = false; while ((docline = doc.readLine()) != null) { if (replacing) { if (docline.trim().equals(endDelim)) replacing = false; else continue; } b.append(docline); if (!replaced_once && docline.trim().equals(startDelim)) { if (formatJavadoc) b.append(optionsToJavadoc(docline.indexOf('*'))); else b.append(optionsToHtml()); replaced_once = true; replacing = true; } } doc.close(); return b.toString(); } // HTML and Javadoc processing methods /** * Process each option and add in the Javadoc info. */ public void processJavadoc() { for (Options.OptionInfo oi : options.getOptions()) { ClassDoc opt_doc = root.classNamed(oi.get_declaring_class().getName()); if (opt_doc != null) { String nameWithUnderscores = oi.long_name.replace('-', '_'); for (FieldDoc fd : opt_doc.fields()) { if (fd.name().equals (nameWithUnderscores)) { // If Javadoc for field is unavailable, then use the @Option // description in the documentation. if (fd.getRawCommentText().length() == 0) { // Input is a string rather than a Javadoc (HTML) comment so we // must escape it. oi.jdoc = StringEscapeUtils.escapeHtml(oi.description); } else if (formatJavadoc) { oi.jdoc = fd.commentText(); } else { oi.jdoc = javadocToHtml(fd); } break; } } } } } /** * Get the HTML documentation for the underlying options instance. */ public String optionsToHtml() { StringBuilderDelimited b = new StringBuilderDelimited(eol); if (includeClassDoc) { b.append(OptionsDoclet.javadocToHtml(root.classes()[0])); b.append("<p>Command line options: </p>"); } b.append("<ul>"); if (!options.isUsingGroups()) { b.append(optionListToHtml(options.getOptions(), 2)); } else { for (Options.OptionGroupInfo gi : options.getOptionGroups()) { // Do not include groups without publicized options in output if (!gi.any_publicized()) continue; b.append(" <li>" + gi.name); b.append(" <ul>"); b.append(optionListToHtml(gi.optionList, 6)); b.append(" </ul>"); b.append(" </li>"); } } b.append("</ul>"); for (Options.OptionInfo oi : options.getOptions()) { if (oi.list != null && !oi.unpublicized) { b.append(list_help); break; } } return b.toString(); } /** * Get the HTML documentation for the underlying options instance, formatted * as a Javadoc comment. */ public String optionsToJavadoc(int padding) { StringBuilderDelimited b = new StringBuilderDelimited(eol); Scanner s = new Scanner(optionsToHtml()); while (s.hasNextLine()) { StringBuilder bb = new StringBuilder(); bb.append(StringUtils.repeat(" ", padding)).append("* ").append(s.nextLine()); b.append(bb); } return b.toString(); } /** * Get the HTML describing many options, formatted as an HTML list. */ private String optionListToHtml(List<Options.OptionInfo> opt_list, int padding) { StringBuilderDelimited b = new StringBuilderDelimited(eol); for (Options.OptionInfo oi : opt_list) { if (oi.unpublicized) continue; StringBuilder bb = new StringBuilder(); String optHtml = optionToHtml(oi); bb.append(StringUtils.repeat(" ", padding)); bb.append("<li>").append(optHtml).append("</li>"); b.append(bb); } return b.toString(); } /** * Get the line of HTML describing an Option. */ public String optionToHtml(Options.OptionInfo oi) { StringBuilder b = new StringBuilder(); Formatter f = new Formatter(b); if (oi.short_name != null) f.format("<b>-%s</b> ", oi.short_name); for (String a : oi.aliases) f.format("<b>%s</b> ", a); String prefix = getUseSingleDash() ? "-" : "--"; f.format("<b>%s%s=</b><i>%s</i>", prefix, oi.long_name, oi.type_name); if (oi.list != null) b.append(" <tt>[+]</tt>"); b.append(". "); String jdoc = oi.jdoc == null ? "" : oi.jdoc; // FIXME: suppress nullness warnings if (oi.no_doc_default || oi.default_str == null) { f.format("%s", jdoc); } else { String default_str = "default " + oi.default_str; // The default string must be HTML escaped since it comes from a string // rather than a Javadoc comment. f.format("%s [%s]", jdoc, StringEscapeUtils.escapeHtml(default_str)); } return b.toString(); } /** * Replace the @link tags and block @see tags in a Javadoc comment with * sensible, non-hyperlinked HTML. This keeps most of the information in the * comment while still being presentable. <p> * * This is only a temporary solution. Ideally, @link/@see tags would be * converted to HTML links which point to actual documentation. */ public static String javadocToHtml(Doc doc) { StringBuilder b = new StringBuilder(); Tag[] tags = doc.inlineTags(); for (Tag tag : tags) { if (tag instanceof SeeTag) b.append("<code>" + tag.text() + "</code>"); else b.append(tag.text()); } SeeTag[] seetags = doc.seeTags(); if (seetags.length > 0) { b.append(" See: "); StringBuilderDelimited bb = new StringBuilderDelimited(", "); for (SeeTag tag : seetags) bb.append("<code>" + tag.text() + "</code>"); b.append(bb); b.append("."); } return b.toString(); } // Getters and Setters public boolean getFormatJavadoc() { return formatJavadoc; } public void setFormatJavadoc(boolean val) { if (val && !formatJavadoc) { startDelim = "* " + startDelim; endDelim = "* " + endDelim; } else if (!val && formatJavadoc) { startDelim = StringUtils.removeStart("* ", startDelim); endDelim = StringUtils.removeStart("* ", endDelim); } this.formatJavadoc = val; } public boolean getUseSingleDash() { return options.isUsingSingleDash(); } public void setUseSingleDash(boolean val) { options.use_single_dash(true); } }