/* gnu.classpath.tools.doclets.xmldoclet.Driver
   Copyright (C) 2001 Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

GNU Classpath is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */

package gnu.classpath.tools.doclets.xmldoclet;

import com.sun.javadoc.*;
import java.io.*;

import com.sun.tools.doclets.Taglet;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import java.text.DateFormat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.TreeSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.HashMap;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeMap;

import gnu.classpath.tools.gjdoc.TemporaryStore;
import gnu.classpath.tools.gjdoc.GjdocPackageDoc;

import gnu.classpath.tools.doclets.PackageGroup;
import gnu.classpath.tools.doclets.PackageMatcher;
import gnu.classpath.tools.doclets.InvalidPackageWildcardException;

import gnu.classpath.tools.doclets.xmldoclet.doctranslet.DocTranslet;
import gnu.classpath.tools.doclets.xmldoclet.doctranslet.DocTransletOptions;

import gnu.classpath.tools.taglets.AuthorTaglet;
import gnu.classpath.tools.taglets.VersionTaglet;
import gnu.classpath.tools.taglets.SinceTaglet;
import gnu.classpath.tools.taglets.DeprecatedTaglet;
import gnu.classpath.tools.taglets.GenericTaglet;
import gnu.classpath.tools.doclets.StandardTaglet;

import gnu.classpath.tools.java2xhtml.Java2xhtml;

import gnu.classpath.tools.IOToolkit;
import gnu.classpath.tools.FileSystemClassLoader;

/**
 *  A Doclet which retrieves all information presented by the Doclet
 *  API, dumping it to stdout in XML format.
 *
 *  @author Julian Scheid
 */
public class Driver {

   public static final String XMLDOCLET_VERSION = "0.6.1";

   /**
    *  Used for redirecting error messages to <code>/dev/null</code>.
    */
   private static class NullErrorReporter implements DocErrorReporter {
      public void printError(String ignore) {}
      public void printWarning(String ignore) {}
      public void printNotice(String ignore) {}
   }

   /*
    *  Taglet context constants.
    */
   private static final int CONTEXT_CONSTRUCTOR  = 1;
   private static final int CONTEXT_FIELD        = 2;
   private static final int CONTEXT_METHOD       = 3;
   private static final int CONTEXT_OVERVIEW     = 4;
   private static final int CONTEXT_PACKAGE      = 5;
   private static final int CONTEXT_TYPE         = 6;

   /**
    *  All XML output will go to this stream.
    */
   private PrintWriter out;

   /**
    *  How many spaces to indent each XML node level,
    *  i.e. Tab size for output.
    */
   private static int indentStep = 1;

   /**
    *  Won't output superfluous spaces if set to true.
    *  If set to false, output will be more legible.
    */
   private boolean compress = false;

   /**
    *  Won't output warning messages while fixing
    *  HTML code if set to true.
    */
   private boolean noHTMLWarn = false;

   /**
    *  Won't output warning messages when encountering tags
    *  that look like an email address if set to true.
    */
   private boolean noEmailWarn = false;

   /**
    *  Will fix HTML if necessary so that each comment
    *  contains valid XML code if set to true. If set
    *  to false, HTML code will not be modified and
    *  instead encapsulated in a CDATA section.
    */
   private boolean fixHTML = true;

   /**
    *  User-specified name of the directory where the final version of
    *  the generated files will be written to.
    *
    *  If no XSLT sheet is given, the XML output will go directly into
    *  this directory. Otherwise, XML output will go to a temporary
    *  directory and XSLT output will go to this directory.
    */
   private File targetDirectory = null;

   /**
    *  Directory where XML output will be written to. If no XSLT
    *  sheet was given, this is the target directory specified
    *  by the user. Otherwise, this is a temporary directory.
    */
   private File xmlTargetDirectory;

   /**
    *  Contains a number of TargetContexts which describe which XSLT
    *  sheet to apply to the output of this doclet, to what directory
    *  the XSLT output is written, and which postprocess driver to use
    *  to process XSLT output.
    */
   private List targets = new ArrayList();

   /**
    *  XML text to include at the end of every generated page. Read
    *  from the file specified on the command line using -bottomnote.
    *  If present, this will be written to the main output file
    *  (index.xml) in node /gjdoc:rootDoc/gjdoc:bottomnote.
    */
   private String bottomNote;

   /**
    *  Brief description of the package set. Can be specified on the
    *  command line using -title.  This will be written to the main
    *  output file (index.xml) in node
    *  /gjdoc:rootDoc/gjdoc:title. The HTML generating XSLT sheet
    *  uses this for example in window titles.
    */
   private String title;

   /**
    *  Path to the directory where temporary files should be stored.
    *  Defaults to system tempdir, but can be overridden by user
    *  with -workpath.
    */
   private String workingPath = System.getProperty("java.io.tmpdir");

   /**
    *  Temporary directory created by this doclet where all
    *  temporary files will be stored in. If no temporary
    *  files are needed (i.e. no XSLT postprocessing stage
    *  specified by user), this is <code>null</code>.
    */
    private File workingDirectory;

   /**
    *  Whether to deep-copy the doc-files subdirectory.
    */
    private boolean docFilesSubdirsEnabled = false;

   /**
    *  Which direct subdirectories of the doc-files directories to exclude.
    *  Set of String.
    */
    private Set excludeDocFilesSubDirs = new HashSet();

   /**
    *  Stores the Doclet API RootDoc we are operating on.
    */
   private RootDoc rootDoc;

   /**
    *  XML namespace prefix used for all tags, except for HTML
    *  tags copied from Javadoc comments. Excluding colon.
    */
   public static final String tagPrefix = "gjdoc";

   /**
    *  Classpath for loading Taglet classes.
    */
   private String tagletPath = null;

   /**
    *  The current class that is being processed.
    *  Set in outputClassDoc().
    */
   private ClassDoc currentClass;

   /**
    *  The current member that is being processed.
    *  Set in outputMemberDoc().
    */
   private MemberDoc currentMember;

   /**
    *  The current constructor/method that is being processed.
    *  Set in outputExecutableMemberDoc().
    */
   private ExecutableMemberDoc currentExecMember;

   /**
    *  Mapping from tag type to Taglet for user Taglets specified on
    *  the command line.
    */
   private Map tagletMap = new LinkedHashMap();

   /**
    *  Keeps track of the tags mentioned by the user during option
    *  processiong so that an error can be emitted if a tag is
    *  mentioned more than once.
    */
   private List mentionedTags = new LinkedList();

   /**
    *  Stores options to be passed to the DocTranslet.
    */
   private DocTransletOptions docTransletOptions = new DocTransletOptions();

   /**
    *  Stores the package groups specified in the user
    *  options. Contains objects of type PackageGroup.
    */
   private List packageGroups = new LinkedList();

   private HtmlRepairer htmlRepairer;

   public static boolean start(TemporaryStore _rootDocWrapper) {
      return new Driver().instanceStart((RootDoc)_rootDocWrapper.getAndClear());
   }

   /**
    *  Official Doclet entry point.
    */
   public static boolean start(RootDoc _rootDoc) {

      // Create a new XmlDoclet instance and delegate control.
      TemporaryStore tstore = new TemporaryStore(_rootDoc);
      _rootDoc = null;
      return new Driver().instanceStart((RootDoc)tstore.getAndClear());
   }

   /**
    *  Output an XML tag describing a com.sun.javadoc.Type object.
    *  Assumes that the tag does not have subtags.
    *
    *  @param level  Level of indentation. Will be multiplied by
    *                <code>indentStep</code> to yield actual amount
    *                of whitespace inserted at start of line.
    *  @param tag    Identifier for the XML tag being output.
    *  @param type   The Javadoc Type to be output.
    */
   protected void outputType(int level, String tag, Type type) {
      outputType(level, tag, type, true);
   }

   protected void outputType(int level, String tag, Type type, boolean atomic) {

      boolean isIncluded = false;
      ClassDoc typeAsClassDoc = type.asClassDoc();
      String packageName = null;
      if (null != typeAsClassDoc) {
         isIncluded = typeAsClassDoc.isIncluded();
         packageName = typeAsClassDoc.containingPackage().name();
      }
      println(level, "<"+tagPrefix+":"+tag + " typename=\""+type.typeName()+"\""+
              " qualifiedtypename=\""+type.qualifiedTypeName()+"\""
              +(type.dimension().length()==0?"":" dimension=\""+type.dimension()+"\"")
              +(isIncluded?" isIncluded=\"true\"" : "")
              +((null != packageName)?" package=\"" + packageName + "\"" : "")
              +(atomic?"/":"")+">");
   }

   protected void outputExecutableMemberDocBody(int level, ExecutableMemberDoc memberDoc) {

      currentExecMember = memberDoc;

      outputMemberDocBody(level, memberDoc);

      Parameter[] parameters = memberDoc.parameters();
      for (int i=0, ilim=parameters.length; i<ilim; ++i) {
         Parameter parameter = parameters[i];
         outputType(level, "parameter name=\""+parameter.name()+"\"", parameter.type());
      }

      ClassDoc[] exceptions = memberDoc.thrownExceptions();
      for (int i=0, ilim=exceptions.length; i<ilim; ++i) {
         ClassDoc exception = exceptions[i];
         outputType(level, "thrownException", exception);
       }

      printAtomTag(level, "signature full=\""+memberDoc.signature()+"\" flat=\""+memberDoc.flatSignature()+"\"");

      if (memberDoc.isNative()) {
         printAtomTag(level, "isNative");
      }

      if (memberDoc.isSynchronized()) {
         printAtomTag(level, "isSynchronized");
      }
   }

   protected void outputMethodDoc(int level, MethodDoc methodDoc) {
      println();
      printOpenTag(level, "methoddoc name=\""+methodDoc.name()+"\"");
      outputExecutableMemberDocBody(level+1, methodDoc);
      outputType(level+1, "returns", methodDoc.returnType());
      printCloseTag(level, "methoddoc");
   }

   protected void outputMemberDocBody(int level, MemberDoc memberDoc) {
      currentMember = memberDoc;
      outputProgramElementDocBody(level, memberDoc);
   }

   protected void outputFieldDocBody(int level, FieldDoc fieldDoc) {
      outputType(level, "type", fieldDoc.type());
      if (fieldDoc.isTransient()) {
         printAtomTag(level, "isTransient");
      }
      if (fieldDoc.isVolatile()) {
         printAtomTag(level, "isVolatile");
      }
   }

   private void outputFieldDoc(int level, FieldDoc fieldDoc) {
      println();
      printOpenTag(level, "fielddoc name=\""+fieldDoc.name()+"\"");
      outputMemberDocBody(level+1, fieldDoc);
      outputFieldDocBody(level+1, fieldDoc);
      printCloseTag(level, "fielddoc");
   }

   protected void outputConstructorDoc(int level, ConstructorDoc constructorDoc) {
      println();
      printOpenTag(level, "constructordoc name=\""+constructorDoc.name()+"\"");
      outputExecutableMemberDocBody(level+1, constructorDoc);
      printCloseTag(level, "constructordoc");
   }

   protected void outputSuperInterfacesRec(int level, ClassDoc classDoc) {
      if (null!=classDoc) {
         ClassDoc[] interfaces = classDoc.interfaces();
         if (null != interfaces) {
            for (int i=0, ilim=interfaces.length; i<ilim; ++i) {
               outputType(level, "superimplements", interfaces[i]);
            }
         }
         outputSuperInterfacesRec(level, classDoc.superclass());
      }
   }

   protected void outputClassDocSummary(ClassDoc classDoc) {
      println();
      printOpenTag(1, "classdoc name=\""+classDoc.name()+"\" qualifiedtypename=\""+classDoc.qualifiedName()+"\" isIncluded=\"true\"");
      if (null!=classDoc.superclass()) {
         outputType(2, "superclass", classDoc.superclass());
      }

      ClassDoc[] interfaces = classDoc.interfaces();
      for (int i=0, ilim=interfaces.length; i<ilim; ++i) {
         outputType(2, "implements", interfaces[i]);
      }
      outputSuperInterfacesRec(2, classDoc.superclass());

      printAtomTag(2, "containingPackage name=\""+classDoc.containingPackage().name()+"\"");
      if (classDoc.isError()) {
         printAtomTag(2, "isError");
      }
      if (classDoc.isException()) {
         printAtomTag(2, "isException");
      }
      if (classDoc.isInterface()) {
         printAtomTag(2, "isInterface");
      }
      if (classDoc.isOrdinaryClass()) {
         printAtomTag(2, "isOrdinaryClass");
      }

      printCloseTag(1, "classdoc");
   }

   protected void outputPackageDoc(PackageDoc packageDoc) {
      println();
      printOpenTag(1, "packagedoc name=\""+packageDoc.name()+"\"");
      if (packageDoc.firstSentenceTags().length > 0) {
         printOpenTag(2, "firstSentenceTags", false);
         outputTags(3, packageDoc.firstSentenceTags(), true, CONTEXT_PACKAGE);
         printCloseTag(0, "firstSentenceTags");
         printOpenTag(2, "inlineTags", false);
         outputTags(3, packageDoc.inlineTags(), true, CONTEXT_PACKAGE);
         printCloseTag(0, "inlineTags");
      }

      if (packageDoc.tags().length > 0) {
         printOpenTag(2, "tags");
         outputTags(3, packageDoc.tags(), true, CONTEXT_PACKAGE);
         printCloseTag(2, "tags");
      }

      if (packageDoc.seeTags().length > 0) {
         printOpenTag(2, "seeTags");
         outputTags(3, packageDoc.seeTags(), true, CONTEXT_PACKAGE);
         printCloseTag(2, "seeTags");
      }

      ClassDoc[] allClasses = (ClassDoc[]) packageDoc.allClasses().clone();
      Arrays.sort(allClasses);

      if (false) {
         for (int i = 0, ilim = allClasses.length; i < ilim; ++ i) {
            printAtomTag(2, "containsClass qualifiedtypename=\""+allClasses[i].qualifiedTypeName()+"\"");
         }
      }

      printCloseTag(1, "packagedoc");
   }

   protected void outputClassDoc(ClassDoc classDoc) throws IOException {

      currentClass = classDoc;

      println();
      printOpenTag(1, "classdoc xmlns=\"http://www.w3.org/TR/REC-html40\" xmlns:"+tagPrefix+"=\"http://www.gnu.org/software/cp-tools/gjdocxml\" name=\""+classDoc.name()+"\" qualifiedtypename=\""+classDoc.qualifiedName()+"\"");

      ClassDoc[] interfaces = classDoc.interfaces();
      for (int i=0, ilim=interfaces.length; i<ilim; ++i) {
         outputType(2, "implements", interfaces[i]);
      }
      outputSuperInterfacesRec(2, classDoc.superclass());

      outputProgramElementDocBody(2, classDoc);
      if (classDoc.isAbstract())
         printAtomTag(2, "isAbstract");
      if (classDoc.isSerializable())
         printAtomTag(2, "isSerializable");
      if (classDoc.isExternalizable())
         printAtomTag(2, "isExternalizable");
      if (classDoc.definesSerializableFields()) {
         printAtomTag(2, "definesSerializableFields");
      }

      ConstructorDoc[] constructors = classDoc.constructors();
      for (int i=0, ilim=constructors.length; i<ilim; ++i) {
         outputConstructorDoc(2, constructors[i]);
      }

      MethodDoc[] methods = classDoc.methods();
      for (int i=0, ilim=methods.length; i<ilim; ++i) {
         outputMethodDoc(2, methods[i]);
      }

      FieldDoc[] fields = classDoc.fields();
      for (int i=0, ilim=fields.length; i<ilim; ++i) {
         outputFieldDoc(2, fields[i]);
      }

      if (classDoc.serializableFields().length > 0) {
         printOpenTag(2, "serializableFields");

         FieldDoc[] sfields = classDoc.serializableFields();
         for (int i=0, ilim=sfields.length; i<ilim; ++i) {
            outputFieldDoc(2, sfields[i]);
         }
         printCloseTag(2, "serializableFields");
      }

      Java2xhtml java2xhtml = new Java2xhtml();
      Properties properties = new Properties();
      properties.setProperty("isCodeSnippet", "true");
      properties.setProperty("hasLineNumbers", "true");
      java2xhtml.setProperties(properties);

      if (null == classDoc.containingClass() && docTransletOptions.linksource) {
         printOpenTag(2, "source");
         StringWriter sourceBuffer = new StringWriter();
         File sourceFile = new File(((GjdocPackageDoc)classDoc.containingPackage()).packageDirectory(),
                                    classDoc.name() + ".java");
         FileReader sourceReader = new FileReader(sourceFile);
         IOToolkit.copyStream(sourceReader, sourceBuffer);
         print(java2xhtml.makeHTML(sourceBuffer.getBuffer(), sourceFile.getName()));
         printCloseTag(2, "source");
      }

      ClassDoc superclassDoc = classDoc.superclass();
      while (superclassDoc != null) {
         outputType(2, "superclass", superclassDoc, false);

         // FIXME: remove the following after adjusting the XSLT sheets:
         printAtomTag(3, "containingPackage name=\"" + superclassDoc.containingPackage().name() + "\"");

         MethodDoc[] superMethods = superclassDoc.methods();
         if (null != superMethods) {
            for (int i=0, ilim=superMethods.length; i<ilim; ++i) {
               printAtomTag(3, "methoddoc name=\"" + superMethods[i].name() + "\" signature=\"" + superMethods[i].signature() + "\"");
            }
         }

         FieldDoc[] superFields = superclassDoc.fields();
         if (null != superFields) {
            for (int i=0, ilim=superFields.length; i<ilim; ++i) {
               printAtomTag(3, "fielddoc name=\"" + superFields[i].name() + "\"");
            }
         }
         printCloseTag(2, "superclass");

         superclassDoc = superclassDoc.superclass();
      }

      outputUsage(classDoc, 2);

      printCloseTag(1, "classdoc");

      currentClass = null;
      currentMember = null;
      currentExecMember = null;
   }

   protected int outputHeritageOpen(int level, ClassDoc classDoc) {

      ClassDoc superClassDoc = classDoc.superclass();
      if (null != superClassDoc) {
         level = outputHeritageOpen(level, superClassDoc);
         ++ level;
      }
      outputType(level, "heritage", classDoc, false);
      return level;
   }

   protected void outputHeritageClose(int level, ClassDoc classDoc) {

      ClassDoc superClassDoc = classDoc.superclass();
      if (null != superClassDoc) {
         outputHeritageClose(level + 1, superClassDoc);
      }
      printCloseTag(level, "heritage");
   }

   protected void outputDocBody(int level, Doc doc) {

      int context = CONTEXT_TYPE;

      if (doc.isClass()) {
         printAtomTag(level, "isClass");

         ClassDoc classDoc = (ClassDoc)doc;
         ClassDoc[] classes = rootDoc.classes();
         for (int i=0, ilim=classes.length; i<ilim; ++i) {
            if (classes[i].superclass() == classDoc) {
               outputType(level, "extended-by", classes[i]);
            }
         }

         outputHeritageOpen(level, classDoc);
         outputHeritageClose(level, classDoc);
      }
      if (doc.isConstructor()) {
         printAtomTag(level, "isConstructor");
         context = CONTEXT_CONSTRUCTOR;
      }
      if (doc.isError()) {
         printAtomTag(level, "isError");
      }
      if (doc.isException()) {
         printAtomTag(level, "isException");
      }
      if (doc.isField()) {
         printAtomTag(level, "isField");
         context = CONTEXT_FIELD;
      }
      if (doc.isIncluded()) {
         printAtomTag(level, "isIncluded");
      }
      if (doc.isInterface()) {
         printAtomTag(level, "isInterface");

         ClassDoc classDoc = (ClassDoc)doc;
         ClassDoc[] classes = rootDoc.classes();
         for (int i=0, ilim=classes.length; i<ilim; ++i) {
            ClassDoc[] implementedInterfaces = classes[i].interfaces();
            for (int j=0; j<implementedInterfaces.length; ++j) {
               if (implementedInterfaces[j] == classDoc) {
                  if (classDoc.isInterface()) {
                        outputType(level, "subinterface", classes[i]);
                  }
                  else {
                     outputType(level, "implemented-by", classes[i]);
                  }
                  break;
               }
            }
         }
      }
      if (doc.isMethod()) {
         printAtomTag(level, "isMethod");
         context = CONTEXT_METHOD;
      }
      if (doc.isOrdinaryClass()) {
         printAtomTag(level, "isOrdinaryClass");
      }

      if (doc.inlineTags().length > 0) {
         printOpenTag(level, "inlineTags", false);
         outputTags(level+1, doc.inlineTags(), true, context);
         printCloseTag(0, "inlineTags");
      }

      if (doc.firstSentenceTags().length > 0) {
         printOpenTag(level, "firstSentenceTags", false);
         outputTags(level+1, doc.firstSentenceTags(), true, context);
         printCloseTag(0, "firstSentenceTags");
      }

      if (doc.tags().length > 0) {
         printOpenTag(level, "tags");
         outputTaglets(level+1, doc.tags(), true, context);
         printCloseTag(level, "tags");
      }

      if (doc.seeTags().length > 0) {
         printOpenTag(level, "seeTags");
         outputTags(level+1, doc.seeTags(), true, context);
         printCloseTag(level, "seeTags");
      }

      SourcePosition position = doc.position();
      if (null != position) {
         printAtomTag(level, "position file=\"" + position.file().getAbsolutePath() + "\" line=\"" + position.line() + "\" column=\"" + position.column() + "\"");
      }
   }

   protected void outputProgramElementDocBody(int level, ProgramElementDoc programElementDoc) {
      outputDocBody(level, programElementDoc);
      printAtomTag(level, "containingPackage name=\""+programElementDoc.containingPackage().name()+"\"");
      if (null!=programElementDoc.containingClass()) {
         outputType(level, "containingClass", programElementDoc.containingClass());
      }
      String access;
      if (programElementDoc.isPublic())
         access="public";
      else if (programElementDoc.isProtected())
         access="protected";
      else if (programElementDoc.isPrivate())
         access="private";
      else if (programElementDoc.isPackagePrivate())
         access="package";
      else
         throw new RuntimeException("Huh? "+programElementDoc+" is neither public, protected, private nor package protected.");
      printAtomTag(level, "access scope=\""+access+"\"");
      if (programElementDoc.isFinal())
         printAtomTag(level, "isFinal");
      if (programElementDoc.isStatic())
         printAtomTag(level, "isStatic");
   }

   protected void outputTags(int level, Tag[] tags, boolean descend, int context) {

      for (int i=0; i<tags.length; ++i) {
         outputTag(tags[i], level, descend, context, i == tags.length-1);
      }
   }

   protected void outputTag(Tag tag, int level, boolean descend, int context, boolean lastTag) {

      if (!"Text".equals(tag.name())) {
         printOpenTag(0 /* don't introduce additional whitespace */,
                      "tag kind=\""+tag.kind()+"\" name=\""+tag.name()+"\"", false);
      }
      if (tag instanceof ThrowsTag) {
         ThrowsTag throwsTag = (ThrowsTag)tag;
         if (null!=throwsTag.exception()) {
            outputType(level+1, "exception", throwsTag.exception());
         }
         else {
            StringBuffer sb = new StringBuffer("Exception ");
            sb.append(throwsTag.exceptionName());
            sb.append(" not found in ");
            if (currentExecMember instanceof MethodDoc) {
               MethodDoc m = (MethodDoc)currentExecMember;
               sb.append(m.returnType().typeName());
               sb.append(m.returnType().dimension());
               sb.append(' ');
            }
            sb.append(currentClass.qualifiedName());
            sb.append('.');
            sb.append(currentExecMember.name());
            sb.append('(');
            Parameter[] params = currentExecMember.parameters();
            for (int j=0; j < params.length; j++) {
               sb.append(params[j].type().typeName());
               sb.append(params[j].type().dimension());
               sb.append(' ');
               sb.append(params[j].name());
               if (j != params.length-1)
                  sb.append(", ");
            }
            sb.append(')');
            printWarning(sb.toString());

            printAtomTag(level+1, "exception typename=\""+throwsTag.exceptionName()+"\"");
         }
      }
      else if (tag instanceof ParamTag) {
         ParamTag paramTag = (ParamTag)tag;
         printAtomTag(level+1, "parameter name=\""+paramTag.parameterName()+"\"");
      }

      if (null != tag.text()) {
         //printOpenTag(level+1, "text", false);
         if (fixHTML) {
            print(htmlRepairer.getWellformedHTML(tag.text()));
         }
         else {
            print("<![CDATA["+cdata(tag.text())+"]]>");
         }
         //printCloseTag(0 /* don't introduce additional whitespace */, "text");
      }
      else {
         printWarning("Tag got null text: "+tag);
      }

      if ((descend && ("@throws".equals(tag.name()) || "@param".equals(tag.name()))) || "@deprecated".equals(tag.name())) {
         if (tag.firstSentenceTags().length>0) {
            printOpenTag(level+1, "firstSentenceTags", false);
            outputTags(level+2, tag.firstSentenceTags(), false, context);
            printCloseTag(0, "firstSentenceTags");
         }

         if (tag.inlineTags().length>0) {
            printOpenTag(level+1, "inlineTags", false);
            outputTags(level+2, tag.firstSentenceTags(), false, context);
            printCloseTag(0, "inlineTags");
         }
      }

      if (fixHTML && lastTag) {
         String terminateText = htmlRepairer.terminateText();
         if (null != terminateText && terminateText.length() > 0) {
            print(terminateText);
         }
      }

      if (!"Text".equals(tag.name())) {

         Taglet inlineTaglet = (Taglet)tagletMap.get(tag.name().substring(1));
         if (null != inlineTaglet && inlineTaglet.isInlineTag()) {
            printOpenTag(0, "inlineTagletText", false);
            print(inlineTaglet.toString(tag));
            printCloseTag(0, "inlineTagletText");
         }

         printCloseTag(0, "tag", false);
      }
   }

   void outputTaglets(int level, Tag[] tags, boolean descend, int context)
   {
      for (Iterator it = tagletMap.keySet().iterator(); it.hasNext(); ) {
         String tagName = (String)it.next();
         Object o = tagletMap.get(tagName);
         Taglet taglet = (Taglet)o;

         if (!taglet.isInlineTag()
             && ((context != CONTEXT_CONSTRUCTOR || taglet.inConstructor())
                 || (context != CONTEXT_FIELD || taglet.inField())
                 || (context != CONTEXT_METHOD || taglet.inMethod())
                 || (context != CONTEXT_OVERVIEW || taglet.inOverview())
                 || (context != CONTEXT_PACKAGE || taglet.inPackage())
                 || (context != CONTEXT_TYPE || taglet.inType()))) {

            List tagsOfThisType = new ArrayList();
            for (int i=0, ilim=tags.length; i<ilim; ++i) {
               if (tags[i].name().substring(1).equals(tagName)) {
                  tagsOfThisType.add(tags[i]);
               }
            }

            if (!tagsOfThisType.isEmpty()) {
               Tag[] tagletTags = (Tag[])tagsOfThisType.toArray(new Tag[tagsOfThisType.size()]);
               if (taglet instanceof StandardTaglet) {
                  Iterator tagIterator = tagsOfThisType.iterator();
                  while (tagIterator.hasNext()) {
                     Tag tag = (Tag)tagIterator.next();
                     outputTag(tag, level, descend, context, !tagIterator.hasNext());
                  }
               }
               else {
                  String tagletString = taglet.toString(tagletTags);
                  if (null != tagletString) {
                     printOpenTag(0, "tag name=\"" + tagName + "\" taglet-generated=\"true\"");
                     if (fixHTML) {
                        print(htmlRepairer.getWellformedHTML(tagletString));
                        print(htmlRepairer.terminateText());
                     }
                     else {
                        print("<![CDATA["+cdata(tagletString)+"]]>");
                     }
                     printCloseTag(0, "tag", false);
                  }
               }
            }
         }
      }
   }

   /**
    *  Inofficial entry point. We got an instance here.
    */
   protected boolean instanceStart(RootDoc _rootDoc) {

      this.rootDoc = _rootDoc;
      _rootDoc = null;

      boolean xmlOnly = true;

      // Set the default Taglet order

      registerTaglet(new VersionTaglet());
      registerTaglet(new AuthorTaglet());
      //registerTaglet(new SinceTaglet());
      registerTaglet(new StandardTaglet("deprecated"));
      registerTaglet(new StandardTaglet("see"));
      registerTaglet(new StandardTaglet("param"));

      // Set the built-in Taglet filter

      AuthorTaglet.setTagletEnabled(false);
      VersionTaglet.setTagletEnabled(false);
      SinceTaglet.setTagletEnabled(true);
      DeprecatedTaglet.setTagletEnabled(true);

      try {
         {

            // Process command line options passed through to this doclet

            TargetContext targetContext = null;

            TargetContext htmlTargetContext
               = new TargetContext(DocTranslet.fromClasspath("/doctranslets/html/gjdoc.xsl"),
                                   targetDirectory);

            for (int i=0, ilim=rootDoc.options().length; i<ilim; ++i) {

               String[] option = rootDoc.options()[i];
               String optionTag = option[0];

               if ("-d".equals(optionTag)) {
                  if (null == targetDirectory) {
                     targetDirectory = new File(option[1]);
                  }
                  if (null != targetContext) {
                     targetContext.setTargetDirectory(targetDirectory);
                  }
               }

               else if ("-nofixhtml".equals(optionTag)) {
                  fixHTML = false;
                  printError("-nofixhtml currently not supported.");
                  return false;
               }
               else if ("-compress".equals(optionTag)) {
                  compress = true;
               }
               else if ("-nohtmlwarn".equals(optionTag)) {
                  noHTMLWarn = true;
               }
               else if ("-noemailwarn".equals(optionTag)) {
                  noEmailWarn = true;
               }
               else if ("-indentstep".equals(optionTag)) {
                  indentStep = Integer.parseInt(option[1]);
               }
               else if ("-doctranslet".equals(optionTag)) {
                  targets.add(targetContext = new TargetContext(DocTranslet.fromJarFile(new File(option[1])),
                                                                targetDirectory));
               }
               else if ("-genhtml".equals(optionTag)) {
                  htmlTargetContext.setTargetDirectory(targetDirectory);
                  targets.add(targetContext = htmlTargetContext);
                  xmlOnly = false;
               }
               else if ("-geninfo".equals(optionTag)) {
                  targetContext
                              = new TargetContext(DocTranslet.fromClasspath("/doctranslets/info/gengj.xsl"),
                                                  targetDirectory);
                  targets.add(targetContext);
                  if (!fixHTML) {
                     printNotice("NOTE: -geninfo implies -fixhtml.");
                     fixHTML = true;
                  }
                  xmlOnly = false;
               }
               else if ("-gendocbook".equals(optionTag)) {
                  targetContext = new TargetContext(DocTranslet.fromClasspath("/doctranslets/docbook/gengj.xsl"),
                                                    targetDirectory);
                  targets.add(targetContext);
                  if (!fixHTML) {
                     printNotice("NOTE: -gendocbook implies -fixhtml.");
                     fixHTML = true;
                  }
               }
               else if ("-genpdf".equals(optionTag)) {
                  targetContext
                     = new TargetContext(DocTranslet.fromClasspath("/doctranslets/docbook/gengj.xsl"),
                                         targetDirectory);
                                         /** "gnu.classpath.tools.doclets.xmldoclet.DocBookPostprocessor") **/
                  targets.add(targetContext);
                  if (!fixHTML) {
                     printNotice("NOTE: -genpdf implies -fixhtml.");
                     fixHTML = true;
                  }
               }
               else if ("-xmlonly".equals(optionTag)) {
                  xmlOnly = true;
               }
               else if ("-bottomnote".equals(optionTag)) {

                  FileReader reader = new FileReader(option[1]);
                  StringWriter writer = new StringWriter();
                  char[] buf = new char[256];
                  int nread;
                  while ((nread = reader.read(buf)) >= 0) {
                     writer.write(buf, 0, nread);
                  }
                  writer.flush();
                  bottomNote = writer.toString();
                  writer.close();
                  reader.close();
               }
               else if ("-title".equals(optionTag)) {

                  title = option[1];
               }
               else if ("-workpath".equals(optionTag)) {

                  workingPath = option[1];
               }
               else if ("-tagletpath".equals(optionTag)) {

                  if (null == tagletPath) {
                     tagletPath = option[1];
                  }
                  else {
                     tagletPath = tagletPath + File.pathSeparator + option[1];
                  }
               }
               else if ("-taglet".equals(optionTag)) {

                  boolean tagletLoaded = false;

                  String useTagletPath = this.tagletPath;
                  if (null == useTagletPath) {
                     useTagletPath = System.getProperty("java.class.path");
                  }

                  try {
                     Class tagletClass;
                     try {
                        tagletClass
                           = new FileSystemClassLoader(useTagletPath).loadClass(option[1]);
                     }
                     catch (ClassNotFoundException e) {
                        // If not found on specified tagletpath, try default classloader
                        tagletClass
                           = Class.forName(option[1]);
                     }
                     Method registerTagletMethod
                        = tagletClass.getDeclaredMethod("register", new Class[] { java.util.Map.class });

                     if (!registerTagletMethod.getReturnType().equals(Void.TYPE)) {
                        printError("Taglet class '" + option[1] + "' found, but register method doesn't return void.");
                     }
                     else if (registerTagletMethod.getExceptionTypes().length > 0) {
                        printError("Taglet class '" + option[1] + "' found, but register method contains throws clause.");
                     }
                     else if ((registerTagletMethod.getModifiers() & (Modifier.STATIC | Modifier.PUBLIC | Modifier.ABSTRACT)) != (Modifier.STATIC | Modifier.PUBLIC)) {
                        printError("Taglet class '" + option[1] + "' found, but register method isn't public static, or is abstract..");
                     }
                     else {
                        Map tempMap = new HashMap();
                        registerTagletMethod.invoke(null, new Object[] { tempMap });
                        tagletLoaded = true;
                        String name = (String)tempMap.keySet().iterator().next();
                        Taglet taglet = (Taglet)tempMap.get(name);
                        tagletMap.put(name, taglet);
                        mentionedTags.add(taglet);
                     }
                  }
                  catch (NoSuchMethodException e) {
                     printError("Taglet class '" + option[1] + "' found, but doesn't contain the register method.");
                  }
                  catch (SecurityException e) {
                     printError("Taglet class '" + option[1] + "' cannot be loaded: " + e.getMessage());
                  }
                  catch (InvocationTargetException e) {
                     printError("Taglet class '" + option[1] + "' found, but register method throws exception: " + e.toString());
                  }
                  catch (IllegalAccessException e) {
                     printError("Taglet class '" + option[1] + "' found, but there was a problem when accessing the register method: " + e.toString());
                  }
                  catch (IllegalArgumentException e) {
                     printError("Taglet class '" + option[1] + "' found, but there was a problem when accessing the register method: " + e.toString());
                  }
                  catch (ClassNotFoundException e) {
                     printError("Taglet class '" + option[1] + "' cannot be found.");
                  }
                  if (!tagletLoaded) {
                     return false;
                  }
               }
               else if ("-author".equals(optionTag)) {
                  AuthorTaglet.setTagletEnabled(true);
               }
               else if ("-version".equals(optionTag)) {
                  VersionTaglet.setTagletEnabled(true);
               }
               else if ("-nosince".equals(optionTag)) {
                  SinceTaglet.setTagletEnabled(false);
               }
               else if ("-nodeprecated".equals(optionTag)) {
                  DeprecatedTaglet.setTagletEnabled(false);
               }
               else if ("-authormail".equals(optionTag)) {

                  if ("no-replace".equalsIgnoreCase(option[1])) {
                     AuthorTaglet.setEmailReplacementType(AuthorTaglet.EmailReplacement.NO_REPLACEMENT);
                  }
                  else if ("mailto-name".equalsIgnoreCase(option[1])) {
                     AuthorTaglet.setEmailReplacementType(AuthorTaglet.EmailReplacement.MAILTO_NAME);
                  }
                  else if ("name-mailto-address".equalsIgnoreCase(option[1])) {
                     AuthorTaglet.setEmailReplacementType(AuthorTaglet.EmailReplacement.NAME_MAILTO_ADDRESS);
                  }
                  else if ("name-mangled-address".equalsIgnoreCase(option[1])) {
                     AuthorTaglet.setEmailReplacementType(AuthorTaglet.EmailReplacement.NAME_MANGLED_ADDRESS);
                  }
                  else {
                     printError("Invalid value for option '-authortag-email'. Allowed values are:"
                                + " no-replace, mailto-name, name-mailto-address, name-mangled-address.");
                     return false;
                  }
               }
               else if ("-mailmangledot".equals(optionTag)) {
                  AuthorTaglet.setDotReplacement(option[1]);
               }
               else if ("-mailmangleat".equals(optionTag)) {
                  AuthorTaglet.setAtReplacement(option[1]);
               }
               else if ("-docfilessubdirs".equals(optionTag)) {
                  docFilesSubdirsEnabled = true;
               }
               else if ("-excludedocfilessubdir".equals(optionTag)) {
                  StringTokenizer st = new StringTokenizer(option[1]);
                  while (st.hasMoreTokens()) {
                     excludeDocFilesSubDirs.add(st.nextToken());
                  }
               }
               else if ("-nonavbar".equals(optionTag)) {
                  docTransletOptions.nonavbar = true;
               }
               else if ("-noindex".equals(optionTag)) {
                  docTransletOptions.noindex = true;
               }
               else if ("-notree".equals(optionTag)) {
                  docTransletOptions.notree = true;
               }
               else if ("-nocomment".equals(optionTag)) {
                  docTransletOptions.nocomment = true;
               }
               else if ("-nohelp".equals(optionTag)) {
                  docTransletOptions.nohelp = true;
               }
               else if ("-splitindex".equals(optionTag)) {
                  docTransletOptions.splitindex = true;
               }
               else if ("-linksource".equals(optionTag)) {
                  docTransletOptions.linksource = true;
               }
               else if ("-windowtitle".equals(optionTag)) {
                  docTransletOptions.windowtitle = option[1];
               }
               else if ("-helpfile".equals(optionTag)) {
                  docTransletOptions.helpfile = new File(option[1]).toURL().toString();
               }
               else if ("-stylesheetfile".equals(optionTag)) {
                  docTransletOptions.stylesheetfile = new File(option[1]).toURL().toString();
               }
               else if ("-header".equals(optionTag)) {
                  docTransletOptions.header = option[1];
               }
               else if ("-footer".equals(optionTag)) {
                  docTransletOptions.footer = option[1];
               }
               else if ("-bottom".equals(optionTag)) {
                  docTransletOptions.bottom = option[1];
               }
               else if ("-doctitle".equals(optionTag)) {
                  docTransletOptions.doctitle = option[1];
               }
               else if ("-nodeprecatedlist".equals(optionTag)) {
                  docTransletOptions.nodeprecatedlist = true;
               }
               else if ("-uses".equals(optionTag)) {
                  docTransletOptions.uses = true;
               }
               else if ("-group".equals(optionTag)) {
                  if (!processGroupOption(option[1], option[2])) {
                     printError("Invalid package wildcard list in -group option \"" + option[1] + "\" " + option[2]);
                     return false;
                  }
               }
               else if ("-tag".equals(optionTag)) {
                  String tagSpec = option[1];
                  boolean validTagSpec = false;
                  int ndx1 = tagSpec.indexOf(':');
                  if (ndx1 < 0) {
                     Taglet taglet = (Taglet)tagletMap.get(tagSpec);
                     if (null == taglet) {
                        printError("There is no standard tag '" + tagSpec + "'.");
                     }
                     else {
                        if (mentionedTags.contains(taglet)) {
                           printError("Tag '" + tagSpec + "' has been added or moved before.");
                        }
                        else {
                           mentionedTags.add(taglet);

                           // re-append taglet
                           tagletMap.remove(tagSpec);
                           tagletMap.put(tagSpec, taglet);
                        }
                     }
                  }
                  else {
                     int ndx2 = tagSpec.indexOf(':', ndx1 + 1);
                     if (ndx2 > ndx1 && ndx2 < tagSpec.length() - 1) {
                        String tagName = tagSpec.substring(0, ndx1);
                        String tagHead = null;
                        if (tagSpec.charAt(ndx2 + 1) == '\"') {
                           if (tagSpec.charAt(tagSpec.length() - 1) == '\"') {
                              tagHead = tagSpec.substring(ndx2 + 2, tagSpec.length() - 1);
                              validTagSpec = true;
                           }
                        }
                        else {
                           tagHead = tagSpec.substring(ndx2 + 1);
                           validTagSpec = true;
                        }

                        boolean tagScopeOverview = false;
                        boolean tagScopePackages = false;
                        boolean tagScopeTypes = false;
                        boolean tagScopeConstructors = false;
                        boolean tagScopeMethods = false;
                        boolean tagScopeFields = false;
                        boolean tagDisabled = false;

                     tag_option_loop:
                        for (int n=ndx1+1; n<ndx2; ++n) {
                           switch (tagSpec.charAt(n)) {
                           case 'X':
                              tagDisabled = true;
                              break;
                           case 'a':
                              tagScopeOverview = true;
                              tagScopePackages = true;
                              tagScopeTypes = true;
                              tagScopeConstructors = true;
                              tagScopeMethods = true;
                              tagScopeFields = true;
                              break;
                           case 'o':
                              tagScopeOverview = true;
                              break;
                           case 'p':
                              tagScopePackages = true;
                              break;
                           case 't':
                              tagScopeTypes = true;
                              break;
                           case 'c':
                              tagScopeConstructors = true;
                              break;
                           case 'm':
                              tagScopeMethods = true;
                              break;
                           case 'f':
                              tagScopeFields = true;
                              break;
                           default:
                              validTagSpec = false;
                              break tag_option_loop;
                           }
                        }

                        if (validTagSpec) {
                           GenericTaglet taglet
                              = new GenericTaglet(tagName,
                                                  tagHead,
                                                  tagScopeOverview,
                                                  tagScopePackages,
                                                  tagScopeTypes,
                                                  tagScopeConstructors,
                                                  tagScopeMethods,
                                                  tagScopeFields);
                           taglet.setTagletEnabled(!tagDisabled);
                           taglet.register(tagletMap);
                           mentionedTags.add(taglet);
                        }
                     }
                  }
                  if (!validTagSpec) {
                     printError("Value for option -tag must be in format \"<tagname>:Xaoptcmf:<taghead>\".");
                  }
               }
            }

            // Use current directory if target directory hasn't been set.
            if (null == targetDirectory) {
               targetDirectory = new File(System.getProperty("user.dir"));
            }
            if (null != targetContext) {
               targetContext.setTargetDirectory(targetDirectory);
            }

            // It is illegal to specify targets AND -xmlonly.

            if (xmlOnly && targets.size() > 0) {

               printError("You can only specify one of -xmlonly and a target format.");
               return false;
            }

            // If no target was specified and XML only was not
            // requested, use HTML as default target.

            if (!xmlOnly && targets.size() == 0) {
               targets.add(targetContext = htmlTargetContext);
            }

            // Set the same target directory for all output.

            // FIXME: Allow separate target directories for different
            // output formats.

            for (Iterator it = targets.iterator(); it.hasNext(); ) {
               TargetContext t = (TargetContext)it.next();
               t.setTargetDirectory(targetDirectory);
            }

            // Create temporary directory if necessary

            if (xmlOnly) {

               xmlTargetDirectory = targetDirectory;
            }
            else {

               File workingTopDirectory = new File(workingPath);

               workingDirectory = new File(workingTopDirectory, "gjdoc.tmp."+System.currentTimeMillis());

               if (!workingDirectory.mkdir()) {
                  printError("Cannot create temporary directory at "+System.getProperty("java.io.tmpdir"));
                  return false;
               }

               File xmlTempDirectory = new File(workingDirectory, "xmloutput");

               if (!xmlTempDirectory.mkdir()) {
                  printError("Cannot create temporary directory for XML output at "+System.getProperty("java.io.tmpdir"));
                  return false;
               }

               xmlTargetDirectory = xmlTempDirectory;
            }

            // Create target directory if necessary

            if (!targetDirectory.exists()) {
               printNotice("Creating destination directory: \""
                           + targetDirectory + "\"");
               if (!targetDirectory.mkdirs()) {
                  printError("Failed to create destination directory \""
                             + targetDirectory + "\"");
                  return false;
               }
            }

            // Check for deprecation

            boolean hasDeprecatedClasses = false;
            boolean hasDeprecatedInterfaces = false;
            boolean hasDeprecatedExceptions = false;
            boolean hasDeprecatedErrors = false;
            boolean hasDeprecatedMethods = false;
            boolean hasDeprecatedFields = false;

            {
               ClassDoc[] classes = rootDoc.classes();
               for (int i = 0, ilim = classes.length; i < ilim; ++ i) {
                  ClassDoc c = classes[i];
                  Tag[] deprecatedTags = c.tags("deprecated");
                  if (null != deprecatedTags && 0 != deprecatedTags.length) {
                     if (c.isInterface()) {
                        hasDeprecatedInterfaces = true;
                     }
                     else if (c.isException()) {
                        hasDeprecatedExceptions = true;
                     }
                     else if (c.isError()) {
                        hasDeprecatedErrors = true;
                     }
                     else /*if (c.isOrdinaryClass())*/ {
                        hasDeprecatedClasses = true;
                     }
                  }

                  MethodDoc[] methods = c.methods();
                  for (int j = 0, jlim = methods.length; j < jlim; ++ j) {
                     MethodDoc m = methods[j];
                     deprecatedTags = m.tags("deprecated");
                     if (null != deprecatedTags && 0 != deprecatedTags.length) {
                        hasDeprecatedMethods = true;
                     }
                  }

                  FieldDoc[] fields = c.fields();
                  for (int j = 0, jlim = fields.length; j < jlim; ++ j) {
                     FieldDoc f = fields[j];
                     deprecatedTags = f.tags("deprecated");
                     if (null != deprecatedTags && 0 != deprecatedTags.length) {
                        hasDeprecatedFields = true;
                     }
                  }
               }
            }

            htmlRepairer = new HtmlRepairer(rootDoc, noHTMLWarn, noEmailWarn,
                                            currentClass, currentMember,
                                            false);

            collectUsage();

            // Begin XML generation

            printNotice("Writing XML Index file...");

            // Assign output stream

            setTargetFile("index.xml");

            // Output XML document header

            println(0, "<?xml version=\"1.0\"?>");
            println("<!DOCTYPE gjdoc SYSTEM \"dtd/gjdoc.dtd\">");
            println();
            printOpenTag(0, "rootdoc xmlns=\"http://www.w3.org/TR/REC-html40\" xmlns:gjdoc=\"http://www.gnu.org/software/cp-tools/gjdocxml\"");

            println();
            println(1, "<!-- Tags from overview page, if available -->");

            if (rootDoc.firstSentenceTags().length > 0) {
               printOpenTag(2, "firstSentenceTags", false);
               outputTags(3, rootDoc.firstSentenceTags(), true, CONTEXT_PACKAGE);
               printCloseTag(0, "firstSentenceTags");
            }

            if (rootDoc.inlineTags().length > 0) {
               printOpenTag(2, "inlineTags");
               outputTags(3, rootDoc.inlineTags(), true, CONTEXT_PACKAGE);
               printCloseTag(2, "inlineTags");
            }

            if (null != bottomNote) {
               printOpenTag(1, "bottomnote");
               print(bottomNote);
               printCloseTag(1, "bottomnote");
            }

            if (null != title) {
               printOpenTag(1, "title");
               println(2, title);
               printCloseTag(1, "title");
            }

            printOpenTag(1, "created");
            println(2, DateFormat.getDateInstance(DateFormat.LONG, Locale.US).format(new java.util.Date()));
            printCloseTag(1, "created");

            if (hasDeprecatedClasses) printAtomTag(1, "hasDeprecatedClasses");
            if (hasDeprecatedInterfaces) printAtomTag(1, "hasDeprecatedInterfaces");
            if (hasDeprecatedExceptions) printAtomTag(1, "hasDeprecatedExceptions");
            if (hasDeprecatedErrors) printAtomTag(1, "hasDeprecatedErrors");
            if (hasDeprecatedMethods) printAtomTag(1, "hasDeprecatedMethods");
            if (hasDeprecatedFields) printAtomTag(1, "hasDeprecatedFields");

            // Output summary of all classes specified on command line

            println();
            println(1, "<!-- Classes specified by user on command line -->");
            ClassDoc[] specifiedClasses = rootDoc.specifiedClasses();
            for (int i=0, ilim=specifiedClasses.length; i<ilim; ++i) {
               ClassDoc sc = specifiedClasses[i];
               printAtomTag(1, "specifiedclass fqname=\""+sc.qualifiedName()+"\" name=\""+sc.name()+"\"");
            }
            specifiedClasses = null;

            // Output summary of all packages specified on command line

            println();
            println(1, "<!-- Packages specified by user on command line -->");
            PackageDoc[] specifiedPackages = rootDoc.specifiedPackages();
            for (int i=0, ilim=specifiedPackages.length; i<ilim; ++i) {
               PackageDoc sp = specifiedPackages[i];
               printAtomTag(1, "specifiedpackage name=\""+sp.name()+"\"");
            }
            specifiedPackages = null;

            // Output package group information specified on the
            // command line

            println();
            println(1, "<!-- Package groups specified by user on command line -->");
            {
               Iterator packageGroupIt = packageGroups.iterator();
               while (packageGroupIt.hasNext()) {
                  PackageGroup packageGroup = (PackageGroup)packageGroupIt.next();
                  SortedSet groupedPackages = packageGroup.getPackages();
                  if (groupedPackages.isEmpty()) {
                     printWarning("Package group named '"
                                  + packageGroup.getName() + "' didn't match any packages.");
                  }
                  else {
                     printOpenTag(1, "packagegroup name=\"" + packageGroup.getName() + "\"");
                     Iterator groupedPackageIt = groupedPackages.iterator();
                     while (groupedPackageIt.hasNext()) {
                        PackageDoc groupedPackageDoc = (PackageDoc)groupedPackageIt.next();
                        printAtomTag(2, "package name=\"" + groupedPackageDoc.name() + "\"");
                     }
                     printCloseTag(1, "packagegroup");
                  }
               }
               packageGroups = null;
            }

            // Output information on all packages for which documentation
            // has been made available via the Doclet API

            println();
            println(1, "<!-- Documentation for all packages -->");
            PackageDoc[] packages = rootDoc.specifiedPackages();
            for (int i=0, ilim=packages.length; i<ilim; ++i) {
               PackageDoc c = packages[i];
               outputPackageDoc(c);
            }
            packages = null;

            // Output brief summary on all classes for which documentation
            // has been made available via the Doclet API.
            //
            // While this is redundant, it can speed up XSLT
            // processing by orders of magnitude

            println();
            println(1, "<!-- Brief summary for all classes -->");
            ClassDoc[] sumclasses = rootDoc.classes();
            for (int i=0, ilim=sumclasses.length; i<ilim; ++i) {
               ClassDoc c = sumclasses[i];
               outputClassDocSummary(c);
            }
            sumclasses = null;

            // Output closing tag, finish output stream

            println();
            printCloseTag(0, "rootdoc");

            closeTargetFile();

            createIndexByName();



            // Output information on all classes for which documentation
            // has been made available via the Doclet API

            println();
            println(1, "<!-- Documentation for all classes -->");
            ClassDoc[] classes = rootDoc.classes();
            String prevPackageName = null;
            for (int i = 0, ilim = classes.length; i < ilim; ++ i) {
               ClassDoc c = classes[i];

               if (isVerbose()) {
                  printNotice("Writing XML information for "+c.qualifiedName()+"...");
               }
               else {
                  String packageName = c.containingPackage().name();
                  if (null == prevPackageName || !packageName.equals(prevPackageName)) {
                     printNotice("Writing XML information for "+packageName+"...");
                     prevPackageName = packageName;
                  }
               }

               setTargetFile(c.qualifiedName().replace('/','.')+".xml");

               println("<?xml version=\"1.0\"?>");
               println("<!DOCTYPE gjdoc SYSTEM \"dtd/gjdoc.dtd\">");

               outputClassDoc(c);

               closeTargetFile();
            }
            classes = null;
         }

         // Copy DTD files to temporary directory

         // FIXME: try to solve this via jar: URLs. but this will
         // probably break libxmlj compatibility (?)

         String[] resources = new String[] {
            "gjdoc.dtd",
            "gjdoc-alphaindex.dtd",
            "dbcentx.mod",
            "ent/iso-amsa.ent",
            "ent/iso-amsb.ent",
            "ent/iso-amsc.ent",
            "ent/iso-amsn.ent",
            "ent/iso-amso.ent",
            "ent/iso-amsr.ent",
            "ent/iso-box.ent",
            "ent/iso-cyr1.ent",
            "ent/iso-cyr2.ent",
            "ent/iso-dia.ent",
            "ent/iso-grk1.ent",
            "ent/iso-grk2.ent",
            "ent/iso-grk3.ent",
            "ent/iso-grk4.ent",
            "ent/iso-lat1.ent",
            "ent/iso-lat2.ent",
            "ent/iso-num.ent",
            "ent/iso-pub.ent",
            "ent/iso-tech.ent",
         };

         File tempDtdDirectory = new File(xmlTargetDirectory, "dtd");
         File tempDtdEntDirectory = new File(tempDtdDirectory, "ent");

         if ((tempDtdDirectory.exists() || tempDtdDirectory.mkdir())
             && (tempDtdEntDirectory.exists() || tempDtdEntDirectory.mkdir())) {
            for (int i = 0; i < resources.length; ++ i) {
               copyResourceToFile("/dtd/" + resources[i],
                                  new File(tempDtdDirectory, resources[i]));
            }
         }
         else {
            printError("Cannot create temporary directories for DTD data at " + tempDtdDirectory);
            return false;
         }

         // Copy package data-dir directory

         {
            PackageDoc[] packages = rootDoc.specifiedPackages();
            for (int i=0, ilim=packages.length; i<ilim; ++i) {
               PackageDoc c = packages[i];
               if (c instanceof GjdocPackageDoc) {
                  copyPackageDataDir((GjdocPackageDoc)c);
               }
            }
         }

         // All information has been output. Apply stylesheet if given.

         gnu.classpath.tools.gjdoc.Main.releaseRootDoc();

         this.currentClass = null;
         this.currentMember = null;
         this.currentExecMember = null;

         System.gc();

         // From this point we are only operating on files, so we don't
         // need this anymore and can free up some memory

         for (Iterator it = targets.iterator(); it.hasNext(); ) {

            TargetContext target = (TargetContext)it.next();

            // We have XSLT postprocessing, run DocTranslet.

            //DocTranslet docTranslet = DocTranslet.fromClasspath("/doctranslets/html/gjdoc.xsl");

            //docTranslet.setOptions(docTransletOptions);

            target.getDocTranslet().setOptions(docTransletOptions);

            target.getDocTranslet().apply(xmlTargetDirectory,
                                          target.getTargetDirectory(),
                                          rootDoc);
         }

         // Done

         targets = null;

         System.gc();
         Runtime.getRuntime().runFinalization();

         return true;
      }
      catch (Exception e) {

         // Something went wrong. Report to stderr and pass error to
         // Javadoc Reporter

         e.printStackTrace();
         printError(e.toString());

         Throwable rootCause = e.getCause();
         if (null != rootCause) {
            while (null != rootCause.getCause()) {
               rootCause = rootCause.getCause();
            }
            System.err.println("Root cause:");
            rootCause.printStackTrace();
         }

         return false;
      }
      finally {

         // In any case, delete the working directory if we created one

         if (null != workingDirectory) {

            if (!deleteRecursive(workingDirectory)) {
               printWarning("Could not delete temporary directory at "+workingDirectory);
            }
         }

         printNotice("Done.");
      }
   }

   /**
    * Recursively delete the specified directory and its contents,
    * like <code>rm -Rf directory</code>
    *
    * @return <code>true</code> on success
    */
   private static boolean deleteRecursive(File directory) {

      boolean success = true;

      File[] files = directory.listFiles();

      for (int i=0, ilim=files.length; i<ilim; ++i) {

         File file = files[i];

         if (file.isDirectory()) {

            success = deleteRecursive(file) && success;
         }
         else {

            success = file.delete() && success;
         }
      }

      return directory.delete() && success;
   }

   /**
    *  Prints a string to stdout and appends a newline.  Convenience
    *  method.
    */
   protected void println(String str) {
      out.println(str);
   }

   /**
    *  Prints a string to stdout without appending a newline.
    *  Convenience method.
    */
   protected void print(String str) {
      out.print(str);
   }

   /**
    *  In standard mode, prints an empty line to stdout.
    *  In thight mode, nothing happens.
    *  Convenience method.
    */
   protected void println() {
      if (!compress) {
         out.println();
      }
   }

   /**
    *  In standard mode, prints the given text indented to stdout and appends newline.
    *  In tight mode, doesn't print indentation or newlines.
    */
   protected void print(int indentLevel, String msg) {
      if (compress) {
         out.print(msg);
      }
      else {
         StringBuffer indentation = new StringBuffer();
         for (int i=0; i<indentLevel*indentStep; ++i) {
            indentation.append(' ');
         }
         out.print(indentation+msg);
      }
   }

   /**
    *  In tight mode, prints a message at a given indentation level.
    *  In standard mode, appends a newline in addition.
    */
   protected void println(int indentLevel, String msg) {
      print(indentLevel, msg);
      if (!compress) out.println();
   }

   /**
    *  Prints an atom tag at the given indentation level.
    */
   protected void printAtomTag(int level, String tag) {
      println(level, "<"+tagPrefix+":"+replaceCharsInTag(tag)+"/>");
   }

   /**
    *  Prints an open tag at the given indentation level.
    */
   protected void printOpenTag(int level, String tag) {
      printOpenTag(level, replaceCharsInTag(tag), true);
   }

   /**
    *  Prints an open tag at the given indentation level and
    *  conditionally appends a newline (if not in tight mode).
    */
   protected void printOpenTag(int level, String tag, boolean appendNewline) {
      if (appendNewline && !compress) {
         println(level, "<"+tagPrefix+":"+replaceCharsInTag(tag)+">");
      }
      else {
         print(level, "<"+tagPrefix+":"+replaceCharsInTag(tag)+">");
      }
   }

   /**
    *  Prints a close tag at the given indentation level.
    */
   protected void printCloseTag(int level, String tag) {
      printCloseTag(level, tag, true);
   }

   /**
    *  Prints a close tag at the given indentation level and
    *  conditionally appends a newline (if not in tight mode).
    */
   protected void printCloseTag(int level, String tag, boolean appendNewline) {
      if (appendNewline && !compress) {
         println(level, "</"+tagPrefix+":"+replaceCharsInTag(tag)+">");
      }
      else {
         print(level, "</"+tagPrefix+":"+replaceCharsInTag(tag)+">");
      }
   }

   public static int optionLength(String option) {
      if ("-d".equals(option)) return 2;
      else if ("-fixhtml".equals(option)) return 1;
      else if ("-compress".equals(option)) return 1;
      else if ("-nohtmlwarn".equals(option)) return 1;
      else if ("-noemailwarn".equals(option)) return 1;
      else if ("-indentstep".equals(option)) return 2;
      else if ("-xslsheet".equals(option)) return 2;
      else if ("-xsltdriver".equals(option)) return 2;
      else if ("-postprocess".equals(option)) return 2;
      else if ("-genhtml".equals(option)) return 1;
      else if ("-geninfo".equals(option)) return 1;
      else if ("-gendocbook".equals(option)) return 1;
      else if ("-xmlonly".equals(option)) return 1;
      else if ("-bottomnote".equals(option)) return 2;
      else if ("-workpath".equals(option)) return 2;
      else if ("-title".equals(option)) return 2;
      else if ("-tagletpath".equals(option)) return 2;
      else if ("-taglet".equals(option)) return 2;
      else if ("-authormail".equals(option)) return 2;
      else if ("-mailmangledot".equals(option)) return 2;
      else if ("-mailmangleat".equals(option)) return 2;
      else if ("-noindex".equals(option)) return 1;
      else if ("-nocomment".equals(option)) return 1;
      else if ("-notree".equals(option)) return 1;
      else if ("-nohelp".equals(option)) return 1;
      else if ("-nonavbar".equals(option)) return 1;
      else if ("-splitindex".equals(option)) return 1;
      else if ("-author".equals(option)) return 1;
      else if ("-version".equals(option)) return 1;
      else if ("-nosince".equals(option)) return 1;
      else if ("-nodeprecated".equals(option)) return 1;
      else if ("-linksource".equals(option)) return 1;
      else if ("-windowtitle".equals(option)) return 2;
      else if ("-helpfile".equals(option)) return 2;
      else if ("-stylesheetfile".equals(option)) return 2;
      else if ("-tag".equals(option)) return 2;
      else if ("-header".equals(option)) return 2;
      else if ("-footer".equals(option)) return 2;
      else if ("-bottom".equals(option)) return 2;
      else if ("-doctitle".equals(option)) return 2;
      else if ("-nodeprecatedlist".equals(option)) return 1;
      else if ("-uses".equals(option)) return 1;
      else if ("-group".equals(option)) return 3;

      else return -1;
   }

   public static boolean validOptions(String[][] options) {
      return true;
   }


   /**
    *  Workaround for non well-formed comments: fix tag contents
    *  by replacing <code>&lt;</code> with <code>&amp;lt;</code>,
    *  <code>&gt;</code> with <code>&amp;gt;</code> and
    *  <code>&amp;</code> with <code>&amp;amp;</code>.
    *
    *  @param tagContent  String to process
    *
    *  @return given String with all special characters replaced by
    *          HTML entities.
    */
   private static String replaceCharsInTag(String tagContent) {
      return
         replaceString(
            replaceString(
               replaceString(
                  tagContent,
                  "<", "&lt;"
                  ),
               ">", "&gt;"
               ),
            "&", "&amp;"
            );
   }

   /**
    *  Replaces all occurences of string <code>needle</code> within string
    *  <code>haystack</code> by string <code>replacement</code>.
    *
    *  @param haystack    The string to search and replace in.
    *  @param needle      The string which is searched for.
    *  @param replacement The string by which every occurence of <code>needle</code> is replaced.
    */
   private static String replaceString(String haystack, String needle, String replacement) {
      int ndx = haystack.indexOf(needle);
      if (ndx<0)
         return haystack;
      else
         return haystack.substring(0, ndx) + replacement
            + replaceString(haystack.substring(ndx+needle.length()), needle, replacement);
   }

   protected void setTargetFile(String filename) throws IOException {

      OutputStream fileOut = new FileOutputStream(new File(xmlTargetDirectory, filename));
      out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(fileOut, "UTF8")));;
   }

   protected void closeTargetFile() {

      out.flush();
      out.close();
   }

   private String cdata(String str) {

      if (null==str) {
         return str;
      } // end of if ((null==str)

      StringBuffer rc = new StringBuffer();
      for (int i=0; i<str.length(); ++i) {
         char c = str.charAt(i);
         if (c==0x09 || c==0x0a || c==0x0d || (c>=0x20 && c<=0xd7ff) || (c>=0xe000 && c<=0xfffd) || (c>=0x10000 && c<=0x10ffff)) {
            rc.append(c);
         }
         else {
            printWarning("Invalid Unicode character 0x"+Integer.toString(c, 16)+" in javadoc markup has been stripped.");
         } // end of else

      }
      return rc.toString();
   }

   static void copyResourceToFile(String resourceName, File target) throws IOException {

      InputStream in = Driver.class.getResourceAsStream(resourceName);

      if (null != in) {

         FileOutputStream out = new FileOutputStream(target);
         int size;
         byte[] buffer = new byte[512];
         while ((size = in.read(buffer)) >= 0) {
            out.write(buffer, 0, size);
         }
         out.close();
      }
      else {

         throw new IOException("Can't find resource named "+resourceName);
      }
   }

   private void printError(String error) {
      if (null != rootDoc) {
         rootDoc.printError(error);
      }
      else {
         System.err.println("ERROR: "+error);
      }
   }

   private void printWarning(String warning) {
      if (null != rootDoc) {
         rootDoc.printWarning(warning);
      }
      else {
         System.err.println("WARNING: "+warning);
      }
   }

   private void printNotice(String notice) {
      if (null != rootDoc) {
         rootDoc.printNotice(notice);
      }
      else {
         System.err.println(notice);
      }
   }

   /**
    *  Copy the contents of the input directory to the output
    *  directory. The output directory must exist.
    */
   private void copyPackageDataDir(GjdocPackageDoc packageDoc) throws IOException {
      File docFilesSourceDirectory
         = new File(packageDoc.packageDirectory(), "doc-files");
      File docFilesTargetDirectory
         = new File(this.targetDirectory,
                    packageDoc.name().replace('.', File.separatorChar));
      if (docFilesSourceDirectory.exists()) {
         printNotice("Copying files from " + docFilesSourceDirectory);
         copyDirectory(docFilesSourceDirectory, docFilesTargetDirectory,
                       docFilesSubdirsEnabled,
                       excludeDocFilesSubDirs);
      }
   }

   /**
    *  Recursively copy the contents of the input directory to the
    *  output directory. The output directory must exist.
    */
   private static void copyDirectory(File sourceDir, File targetDir,
                                     boolean recursive,
                                     Set excludeDirs) throws IOException {
      if (!targetDir.exists() && !targetDir.mkdirs()) {
         throw new IOException("Cannot create directory " + targetDir);
      }

      File[] sourceFiles = sourceDir.listFiles();
      for (int i=0; i<sourceFiles.length; ++i) {
         if (sourceFiles[i].isDirectory()) {
            if (recursive && (null == excludeDirs
                              || !excludeDirs.contains(sourceFiles[i].getName()))) {
               File targetSubDir = new File(targetDir,
                                            sourceFiles[i].getName());
               if (targetSubDir.exists() || targetSubDir.mkdir()) {
                  copyDirectory(sourceFiles[i], targetSubDir, recursive, null);
               }
               else {
                  throw new IOException("Cannot create directory " + targetSubDir);
               }
            }
         }
         else {
            copyFile(sourceFiles[i], new File(targetDir, sourceFiles[i].getName()));
         }
      }
   }

   /**
    *  Copy the contents of the input file to the output file. The
    *  output file's parent directory must exist.
    */
   private static void copyFile(File sourceFile, File targetFile) throws IOException {

      InputStream in = new FileInputStream(sourceFile);
      OutputStream out = new FileOutputStream(targetFile);
      int nread;
      byte[] buf = new byte[512];
      while ((nread = in.read(buf)) >= 0) {
         out.write(buf, 0, nread);
      }
      in.close();
      out.close();
   }

   private void createIndexByName() throws IOException {
      // Create index

      // Collect index

      Map indexMap = new TreeMap(new Comparator() {
            public int compare(Object o1, Object o2) {
               return o1.toString().toLowerCase().compareTo(o2.toString().toLowerCase());
            }
         });

      // Add packages to index

      PackageDoc[] packages = rootDoc.specifiedPackages();
      for (int i=0, ilim=packages.length; i<ilim; ++i) {
         PackageDoc c = packages[i];
         indexMap.put(c.name(), c);
      }

      // Add classes, fields and methods to index

      ClassDoc[] sumclasses = rootDoc.classes();
      for (int i=0, ilim=sumclasses.length; i<ilim; ++i) {
         ClassDoc c = sumclasses[i];
         if (null == c.containingClass()) {
            indexMap.put(c.name(), c);
         }
         else {
            indexMap.put(c.name().substring(c.containingClass().name().length() + 1), c);
         }
         FieldDoc[] fields = c.fields();
         for (int j=0, jlim=fields.length; j<jlim; ++j) {
            indexMap.put(fields[j].name(), fields[j]);
         }
         MethodDoc[] methods = c.methods();
         for (int j=0, jlim=methods.length; j<jlim; ++j) {
            MethodDoc method = methods[j];
            StringBuffer signature = new StringBuffer();
            signature.append(method.name());
            signature.append('(');
            Parameter[] parameters = method.parameters();
            for (int k=0, klim=parameters.length; k<klim; ++k) {
               if (k > 0) {
                  signature.append(", ");
               }
               signature.append(parameters[k].typeName());
            }
            signature.append(')');
            indexMap.put(signature.toString(), method);
         }
      }

      // Assign output stream

      setTargetFile("alphaindex.xml");

      // Output XML document header

      println(0, "<?xml version=\"1.0\"?>");
      println("<!DOCTYPE gjdoc SYSTEM \"dtd/gjdoc-alphaindex.dtd\">");
      println();
      printOpenTag(0, "alphaindex xmlns=\"http://www.w3.org/TR/REC-html40\" xmlns:gjdoc=\"http://www.gnu.org/software/cp-tools/gjdocxml\"");

      Iterator it = indexMap.keySet().iterator();

      char previousCategoryLetter = '\0';
      boolean categoryOpen = false;

      while (it.hasNext()) {
         String key = (String)it.next();
         Doc entry = (Doc)indexMap.get(key);

         char firstChar = Character.toUpperCase(key.charAt(0));
         if (firstChar != previousCategoryLetter) {
            if (categoryOpen) {
               printCloseTag(1, "category");
            }
            printOpenTag(1, "category letter=\"" + firstChar + "\"");
            categoryOpen = true;
            previousCategoryLetter = firstChar;
         }

         printOpenTag(2, "entry name=\"" + key + "\"");
         if (entry instanceof PackageDoc) {
            printAtomTag(3, "isPackage");
         }
         else if (entry instanceof ClassDoc) {
            printAtomTag(3, "isClass");
            ClassDoc centry = (ClassDoc)entry;
            currentClass = centry;
            printAtomTag(3, "containingPackage name=\"" + centry.containingPackage().name() + "\"");
            if (null != centry.containingClass()) {
               printAtomTag(3, "containingClass name=\"" + centry.containingClass().name() + "\"");
            }
            if (centry.isInterface()) {
               printAtomTag(3, "isInterface");
            }
            if (centry.isException()) {
               printAtomTag(3, "isException");
            }
            if (centry.isError()) {
               printAtomTag(3, "isError");
            }
            if (centry.isOrdinaryClass()) {
               printAtomTag(3, "isOrdinaryClass");
            }
         }
         else if (entry instanceof ProgramElementDoc) {
            ProgramElementDoc pentry = (ProgramElementDoc)entry;
            currentClass = pentry.containingClass();
            printAtomTag(3, "containingPackage name=\"" + pentry.containingPackage().name() + "\"");
            printAtomTag(3, "containingClass name=\"" + pentry.containingClass().name() + "\"");
            if (pentry.isMethod()) {
               printAtomTag(3, "isMethod");
               ExecutableMemberDoc mentry = (ExecutableMemberDoc)pentry;
               printAtomTag(3, "signature full=\""+mentry.signature()+"\" flat=\""+mentry.flatSignature()+"\"");
               printAtomTag(3, "method name=\"" + mentry.name() + "\"");
            }
            if (pentry.isField()) {
               printAtomTag(3, "isField");
            }
         }

         Tag[] tags = entry.firstSentenceTags();
         for (int i=0, ilim=tags.length; i<ilim; ++i) {
            Tag tag = tags[i];
            if (tag.firstSentenceTags().length>0) {
               printOpenTag(3, "firstSentenceTags", false);
               outputTags(4, tag.firstSentenceTags(), false, CONTEXT_TYPE);
               printCloseTag(3, "firstSentenceTags");
            }
         }


         printCloseTag(2, "entry");
      }

      if (categoryOpen) {
         printCloseTag(1, "category");
      }

      printCloseTag(0, "alphaindex");

      closeTargetFile();
   }

   private static class UsageType
   {
      public static final UsageType CLASS_DERIVED_FROM = new UsageType("class-derived-from");
      public static final UsageType FIELD_OF_TYPE = new UsageType("field-of-type");
      public static final UsageType METHOD_WITH_RETURN_TYPE = new UsageType("method-with-return-type");
      public static final UsageType METHOD_WITH_PARAMETER_TYPE = new UsageType("method-with-parameter-type");
      public static final UsageType METHOD_WITH_THROWN_TYPE = new UsageType("method-with-thrown-type");
      public static final UsageType CONSTRUCTOR_WITH_PARAMETER_TYPE = new UsageType("constructor-with-parameter-type");
      public static final UsageType CONSTRUCTOR_WITH_THROWN_TYPE = new UsageType("constructor-with-thrown-type");
      private String id;

      private UsageType(String id)
      {
         this.id = id;
      }

      public String toString() {
         return "UsageType{id=" + id + "}";
      }

      public String getId() {
         return id;
      }
   }

   /**
    *  ClassDoc -> (PackageDoc -> (UsageType -> (Set of Doc)))
    */
   private Map usedClassToPackagesMap = new HashMap();

   private void addUsedBy(ClassDoc usedClass, UsageType usageType, Doc user, PackageDoc userPackage)
   {
      Map packageToUsageTypeMap = (Map)usedClassToPackagesMap.get(usedClass);
      if (null == packageToUsageTypeMap) {
         packageToUsageTypeMap = new HashMap();
         usedClassToPackagesMap.put(usedClass, packageToUsageTypeMap);
      }

      Map usageTypeToUsersMap = (Map)packageToUsageTypeMap.get(userPackage);
      if (null == usageTypeToUsersMap) {
         usageTypeToUsersMap = new HashMap();
         packageToUsageTypeMap.put(userPackage, usageTypeToUsersMap);
      }

      Set userSet = (Set)usageTypeToUsersMap.get(usageType);
      if (null == userSet) {
         userSet = new TreeSet(); // FIXME: we need the collator from Main here
         usageTypeToUsersMap.put(usageType, userSet);
      }
      userSet.add(user);
   }

   /**
    *  Create the cross reference database.
    */
   private void collectUsage() {

      ClassDoc[] classes = rootDoc.classes();
      for (int i = 0, ilim = classes.length; i < ilim; ++ i) {
         ClassDoc clazz = classes[i];

         // classes derived from
         for (ClassDoc superclass = clazz.superclass(); superclass != null;
              superclass = superclass.superclass()) {
            addUsedBy(superclass, UsageType.CLASS_DERIVED_FROM, clazz, clazz.containingPackage());
         }

         FieldDoc[] fields = clazz.fields();
         for (int j = 0, jlim = fields.length; j < jlim; ++ j) {
            FieldDoc field = fields[j];

            // fields of type
            ClassDoc fieldType = field.type().asClassDoc();
            if (null != fieldType) {
               addUsedBy(fieldType, UsageType.FIELD_OF_TYPE,
                         field, clazz.containingPackage());
            }
         }

         MethodDoc[] methods = clazz.methods();
         for (int j = 0, jlim = methods.length; j < jlim; ++ j) {
            MethodDoc method = methods[j];

            // methods with return type

            ClassDoc returnType = method.returnType().asClassDoc();
            if (null != returnType) {
               addUsedBy(returnType, UsageType.METHOD_WITH_RETURN_TYPE,
                         method, clazz.containingPackage());
            }
            Parameter[] parameters = method.parameters();
            for (int k=0; k<parameters.length; ++k) {

               // methods with parameter type

               Parameter parameter = parameters[k];
               ClassDoc parameterType = parameter.type().asClassDoc();
               if (null != parameterType) {
                  addUsedBy(parameterType, UsageType.METHOD_WITH_PARAMETER_TYPE,
                            method, clazz.containingPackage());
               }
            }

            // methods which throw

            ClassDoc[] thrownExceptions = method.thrownExceptions();
            for (int k = 0, klim = thrownExceptions.length; k < klim; ++ k) {
               ClassDoc thrownException = thrownExceptions[k];
               addUsedBy(thrownException, UsageType.METHOD_WITH_THROWN_TYPE,
                         method, clazz.containingPackage());
            }
         }

         ConstructorDoc[] constructors = clazz.constructors();
         for (int j = 0, jlim = constructors.length; j < jlim; ++ j) {

            ConstructorDoc constructor = constructors[j];

            Parameter[] parameters = constructor.parameters();
            for (int k = 0, klim = parameters.length; k < klim; ++ k) {

               // constructors with parameter type

               Parameter parameter = parameters[k];
               ClassDoc parameterType = parameter.type().asClassDoc();
               if (null != parameterType) {
                  addUsedBy(parameterType, UsageType.CONSTRUCTOR_WITH_PARAMETER_TYPE,
                            constructor, clazz.containingPackage());
               }
            }

            // constructors which throw

            ClassDoc[] thrownExceptions = constructor.thrownExceptions();
            for (int k = 0, klim = thrownExceptions.length; k < klim; ++ k) {
               ClassDoc thrownException = thrownExceptions[k];
               addUsedBy(thrownException, UsageType.CONSTRUCTOR_WITH_THROWN_TYPE,
                         constructor, clazz.containingPackage());
            }
         }
      }
   }

   private void outputUsage(ClassDoc clazz, int level) {

      Map packageToUsageTypeMap = (Map)usedClassToPackagesMap.get(clazz);
      if (null != packageToUsageTypeMap) {
         printOpenTag(level, "references");

         Iterator packagesIterator = packageToUsageTypeMap.keySet().iterator();

         while (packagesIterator.hasNext()) {
            PackageDoc packageDoc = (PackageDoc)packagesIterator.next();
            printOpenTag(level + 1, "referencing-package name=\"" + packageDoc.name() + "\"");
            Map usageTypeToUsersMap = (Map)packageToUsageTypeMap.get(packageDoc);
            Iterator usageTypeIterator = usageTypeToUsersMap.keySet().iterator();
            while (usageTypeIterator.hasNext()) {
               UsageType usageType = (UsageType)usageTypeIterator.next();
               printOpenTag(level + 2, "usage-type id=\"" + usageType.getId() + "\"");
               Set users = (Set)usageTypeToUsersMap.get(usageType);
               Iterator userIterator = users.iterator();
               while (userIterator.hasNext()) {
                  Doc user = (Doc)userIterator.next();
                  if (user instanceof ClassDoc) {
                     printAtomTag(level + 3, "user"
                                  + " class=\"" + ((ClassDoc)user).name() + "\"");
                  }
                  else if (user instanceof FieldDoc) {
                     FieldDoc fieldDoc = (FieldDoc)user;
                     printAtomTag(level + 3, "user"
                                  + " class=\"" + fieldDoc.containingClass().name() + "\""
                                  + " field=\"" + fieldDoc.name() + "\"");
                  }
                  else if (user instanceof MethodDoc) {
                     MethodDoc methodDoc = (MethodDoc)user;
                     printAtomTag(level + 3, "user"
                                  + " class=\"" + methodDoc.containingClass().name() + "\""
                                  + " method=\"" + methodDoc.name() + "\""
                                  + " signature=\"" + methodDoc.signature() + "\""
                                  + " flatSignature=\"" + methodDoc.flatSignature() + "\"");
                  }
                  else if (user instanceof ConstructorDoc) {
                     ConstructorDoc constructorDoc = (ConstructorDoc)user;
                     printAtomTag(level + 3, "user"
                                  + " class=\"" + constructorDoc.containingClass().name() + "\""
                                  + " signature=\"" + constructorDoc.signature() + "\""
                                  + " flatSignature=\"" + constructorDoc.flatSignature() + "\"");
                  }
               }
               printCloseTag(level +2, "usage-type");
            }
            printCloseTag(level + 1, "referencing-package");
         }

         printCloseTag(level, "references");
      }
   }

   private boolean processGroupOption(String groupName, String colonSeparatedPackageList)
   {
      try {
         PackageMatcher packageMatcher = new PackageMatcher();

         StringTokenizer tokenizer = new StringTokenizer(colonSeparatedPackageList, ":");
         while (tokenizer.hasMoreTokens()) {
            String packageWildcard = tokenizer.nextToken();
            packageMatcher.addWildcard(packageWildcard);
         }

         SortedSet groupPackages = packageMatcher.filter(rootDoc.specifiedPackages());

         packageGroups.add(new PackageGroup(groupName, groupPackages));

         return true;
      }
      catch (InvalidPackageWildcardException e) {
         return false;
      }
   }

   private void registerTaglet(Taglet taglet)
   {
      tagletMap.put(taglet.getName(), taglet);
   }

   private boolean isVerbose()
   {
      return false;
   }
}
