/* gnu.classpath.tools.gjdoc.Main
   Copyright (C) 2001, 2012 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.gjdoc;

import com.sun.javadoc.*;
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
import java.text.Collator;

import gnu.classpath.tools.FileSystemClassLoader;

/**
 * Class that will launch the gjdoc tool.
 */
public final class Main
{

  /**
   * Do we load classes that are referenced as base class?
   */
  static final boolean DESCEND_SUPERCLASS = true;

  /**
   * Do we load classes that are referenced as interface?
   */
  static final boolean DESCEND_INTERFACES = false;

  /**
   * Do we load classes that are imported in a source file?
   */
  static final boolean DESCEND_IMPORTED = true;

  /**
   * Document only public members.
   */
  static final int COVERAGE_PUBLIC = 0;

  /**
   * Document only public and protected members.
   */
  static final int COVERAGE_PROTECTED = 1;

  /**
   * Document public, protected and package private members.
   */
  static final int COVERAGE_PACKAGE = 2;

  /**
   * Document all members.
   */
  static final int COVERAGE_PRIVATE = 3;

  /*
   *  FIXME: This should come from a ResourceBundle
   */
  private static final String STRING_TRY_GJDOC_HELP =
     "Try `gjdoc --help' for more information.";

  /**
   * Grid for looking up whether a particular access level is included in the
   * documentation.
   */
  static final boolean[][] coverageTemplates = new boolean[][]
    { new boolean[]
      { true, false, false, false }, // public
        new boolean[]
          { true, true, false, false }, // protected
        new boolean[]
          { true, true, true, false }, // package
        new boolean[]
          { true, true, true, true }, // private
    };

  /**
   * Holds the Singleton instance of this class.
   */
  private static Main instance = new Main();

  /**
   * Avoid re-instantiation of this class.
   */
  private Main()
  {
  }

  private static RootDocImpl rootDoc;

  private ErrorReporter reporter;

  /**
   * Cache for version string from resource /version.properties
   */
  private String gjdocVersion;

  /**
   * <code>false</code> during Phase I: preparation of the documentation data.
   * <code>true</code> during Phase II: documentation output by doclet.
   */
  boolean docletRunning = false;

  //---- Command line options

  /**
   * Option "-doclet": name of the Doclet class to use.
   */
  private String option_doclet = "gnu.classpath.tools.doclets.htmldoclet.HtmlDoclet";

  /**
   * Option "-coverage": which members to include in generated documentation.
   */
  private int option_coverage = COVERAGE_PROTECTED;

  /**
   * Option "-help": display command line usage.
   */
  private boolean option_help;

  /**
   * Option "-docletpath": path to doclet classes.
   */
  private String option_docletpath;

  /**
   * Option "-sourcepath": path to the Java source files to be documented.
   * FIXME: this should be a list of paths
   */
  private List<File> option_sourcepath = new ArrayList<File>();

  /**
   * Option "-locale:" Specify the locale charset of Java source files.
   */
  private Locale option_locale = new Locale("en", "us");

  /**
   * Option "-encoding": Specify character encoding of Java source files.
   */
  private String option_encoding;

  /**
   * Option "-source:" should be 1.4 to handle assertions, 1.1 is no
   * longer supported.
   */
  private String option_source = "1.2";

  /**
   * Option "-subpackages": list of subpackages to be recursively
   * added.
   */
  private List<String> option_subpackages = new ArrayList<String>();

  /**
   * Option "-exclude": list of subpackages to exclude.
   */
  private List<String> option_exclude = new ArrayList<String>();

  /**
   * Option "-breakiterator" - whether to use BreakIterator for
   * detecting the end of the first sentence.
   */
  private boolean option_breakiterator;

  /**
   * Option "-licensetext" - whether to copy license text.
   */
  private boolean option_licensetext;

  /**
   * The locale-dependent collator used for sorting.
   */
  private Collator collator;

  /**
   * true when --version has been specified on the command line.
   */
  private boolean option_showVersion;

  /**
   * true when -bootclasspath has been specified on the command line.
   */
  private boolean option_bootclasspath_specified;

  /**
   * true when -all has been specified on the command line.
   */
  private boolean option_all;

  /**
   * true when -reflection has been specified on the command line.
   */
  private boolean option_reflection;

  // TODO: add the rest of the options as instance variables

  /**
   * Parse all source files/packages and subsequentially start the Doclet given
   * on the command line.
   *
   * @param allOptions List of all command line tokens
   */
  private boolean startDoclet(List<String> allOptions)
  {

    try
    {

      //--- Fetch the Class object for the Doclet.

      Debug.log(1, "loading doclet class...");

      Class<?> docletClass;

      if (null != option_docletpath) {
        try {
          FileSystemClassLoader docletPathClassLoader
            = new FileSystemClassLoader(option_docletpath);
          System.err.println("trying to load class  " + option_doclet + " from path " + option_docletpath);
          docletClass = docletPathClassLoader.findClass(option_doclet);
        }
        catch (Exception e) {
          docletClass = Class.forName(option_doclet);
        }
      }
      else {
        docletClass = Class.forName(option_doclet);
      }
      //Object docletInstance = docletClass.newInstance();

      Debug.log(1, "doclet class loaded...");

      Method startTempMethod = null;
      Method startMethod = null;
      Method optionLenMethod = null;
      Method validOptionsMethod = null;

      //--- Try to find the optionLength method in the Doclet class.

      try
      {
        optionLenMethod = docletClass.getMethod("optionLength", new Class[]
          { String.class });
      }
      catch (NoSuchMethodException e)
      {
        // Ignore if not found; it's OK it the Doclet class doesn't define
        // this method.
      }

      //--- Try to find the validOptions method in the Doclet class.

      try
      {
        validOptionsMethod = docletClass.getMethod("validOptions", new Class[]
          { String[][].class, DocErrorReporter.class });
      }
      catch (NoSuchMethodException e)
      {
        // Ignore if not found; it's OK it the Doclet class doesn't define
        // this method.
      }

      //--- Find the start method in the Doclet class; complain if not found

      try
      {
        startTempMethod = docletClass.getMethod("start", new Class[]
          { TemporaryStore.class });
      }
      catch (Exception e)
      {
        // ignore
      }
      startMethod = docletClass.getMethod("start", new Class[]
        { RootDoc.class });

      //--- Feed the custom command line tokens to the Doclet

      // stores all recognized options
      List<String[]> options = new LinkedList<String[]>();

      // stores packages and classes defined on the command line
      List<String> packageAndClasses = new LinkedList<String>();

      for (Iterator<String> it = allOptions.iterator(); it.hasNext();)
      {
        String option = it.next();

        Debug.log(9, "parsing option '" + option + "'");

        if (option.startsWith("-"))
        {

          //--- Parse option

          int optlen = optionLength(option);

          //--- Try to get option length from Doclet class

          if (optlen <= 0 && optionLenMethod != null)
          {

            optionLenMethod.invoke(null, new Object[]
              { option });

            Debug.log(3, "invoking optionLen method");

            optlen = ((Integer) optionLenMethod.invoke(null, new Object[]
              { option })).intValue();

            Debug.log(3, "done");
          }

          if (optlen <= 0) {

            if (option.startsWith("-JD")) {
              // Simulate VM option -D
              String propertyValue = option.substring(3);
              int ndx = propertyValue.indexOf('=');
              if (ndx <= 0) {
                reporter.printError("Illegal format in option " + option + ": use -JDproperty=value");
                return false;
              }
              else {
                String property = propertyValue.substring(0, ndx);
                String value = propertyValue.substring(ndx + 1);
                System.setProperty(property, value);
              }
            }
            else if (option.startsWith("-J")) {
              //--- Warn if VM option is encountered
              reporter.printWarning("Ignored option " + option + ". Pass this option to the VM if required.");
            }
            else {
              //--- Complain if not found

              reporter.printError("Unknown option " + option);
              reporter.printNotice(STRING_TRY_GJDOC_HELP);
              return false;
            }
          }
          else
          {

            //--- Read option values

            String[] optionAndValues = new String[optlen];
            optionAndValues[0] = option;
            for (int i = 1; i < optlen; ++i)
            {
              if (!it.hasNext())
              {
                reporter.printError("Missing value for option " + option);
                return false;
              }
              else
              {
                optionAndValues[i] = (String) it.next();
              }
            }

            //--- Store option for processing later

            options.add(optionAndValues);
          }
        }
        else if (option.length() > 0)
        {

          //--- Add to list of packages/classes if not option or option
          // value

          packageAndClasses.add(option);
        }
      }

      Debug.log(9, "options parsed...");

      //--- For each package specified with the -subpackages option on
      //         the command line, recursively find all valid java files
      //         beneath it.

      //--- For each class or package specified on the command line,
      //         check that it exists and find out whether it is a class
      //         or a package

      for (Iterator<String> it = option_subpackages.iterator(); it.hasNext();)
      {
        String subpackage =  it.next();
        Set<String> foundPackages = new LinkedHashSet<String>();

        for (Iterator<File> pit = option_sourcepath.iterator(); pit.hasNext(); ) {
          File sourceDir = pit.next();
          File packageDir = new File(sourceDir, subpackage.replace('.', File.separatorChar));
          findPackages(subpackage, packageDir, foundPackages);
        }

        addFoundPackages(subpackage, foundPackages);
      }

      if (option_all) {
        Set<String> foundPackages = new LinkedHashSet<String>();
        for (Iterator<File> pit = option_sourcepath.iterator(); pit.hasNext(); ) {
          File sourceDir = pit.next();
          findPackages("", sourceDir, foundPackages);
        }
        addFoundPackages(null, foundPackages);
        for (Iterator<String> packageIt = foundPackages.iterator(); packageIt.hasNext(); ) {
          String packageName = packageIt.next();
          if (null == packageName) {
            packageName = "";
          }
          rootDoc.addSpecifiedPackageName(packageName);
        }
      }

      for (Iterator<String> it = packageAndClasses.iterator(); it.hasNext();)
      {

        String classOrPackage = it.next();

        boolean foundSourceFile = false;

        if (classOrPackage.endsWith(".java")) {
          for (Iterator<File> pit = option_sourcepath.iterator(); pit.hasNext() && !foundSourceFile; ) {
            File sourceDir = pit.next();
            File sourceFile = new File(sourceDir, classOrPackage);
            if (sourceFile.exists() && !sourceFile.isDirectory()) {
              rootDoc.addSpecifiedSourceFile(sourceFile);
              foundSourceFile = true;
              break;
            }
          }
          if (!foundSourceFile) {
            File sourceFile = new File(classOrPackage);
            if (sourceFile.exists() && !sourceFile.isDirectory()) {
              rootDoc.addSpecifiedSourceFile(sourceFile);
              foundSourceFile = true;
            }
          }
        }

        if (!foundSourceFile) {
        //--- Check for illegal name

        if (classOrPackage.startsWith(".")
            || classOrPackage.endsWith(".")
            || classOrPackage.indexOf("..") > 0
            || !checkCharSet(classOrPackage,
                "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_."))
        {
          throw new ParseException("Illegal class or package name '"
              + classOrPackage + "'");
        }

        //--- Assemble absolute path to package

        String classOrPackageRelPath = classOrPackage.replace('.',
            File.separatorChar);

        //--- Create one file object each for a possible package directory
        //         and a possible class file, and find out if they exist.

        List<File> packageDirs = rootDoc.findSourceFiles(classOrPackageRelPath);
        List<File> sourceFiles = rootDoc.findSourceFiles(classOrPackageRelPath + ".java");

        boolean packageDirExists = !packageDirs.isEmpty();
        boolean sourceFileExists = !sourceFiles.isEmpty();

        //--- Complain if neither exists: not found

        if (!packageDirExists && !sourceFileExists)
        {
          reporter.printError("Class or package " + classOrPackage
              + " not found.");
          return false;
        }

        //--- Complain if both exist: ambigious

        else
          if (packageDirExists && sourceFileExists)
          {
            reporter.printError("Ambigious class/package name "
                + classOrPackage + ".");
            return false;
          }

          //--- Otherwise, if the package directory exists, it is a package

          else
            if (packageDirExists) {
              Iterator<File> packageDirIt = packageDirs.iterator();
              boolean packageDirFound = false;
              while (packageDirIt.hasNext()) {
                File packageDir = packageDirIt.next();
                if (packageDir.isDirectory()) {
                  rootDoc.addSpecifiedPackageName(classOrPackage);
                  packageDirFound = true;
                  break;
                }
              }
              if (!packageDirFound) {
                reporter.printError("No suitable file or directory found for" + classOrPackage);
                return false;
              }
            }

            //--- Otherwise, emit error message

            else {
                reporter.printError("No sources files found for package " + classOrPackage);
            }
        }
      }

      //--- Complain if no packages or classes specified

      if (option_help) {
        usage();
        return true;
      }

      //--- Validate custom options passed on command line
      //         by asking the Doclet if they are OK.

      String[][] customOptionArr = (String[][]) options
          .toArray(new String[0][0]);
      if (validOptionsMethod != null
          && !((Boolean) validOptionsMethod.invoke(null, new Object[]
            { customOptionArr, reporter })).booleanValue())
      {
        // Not ok: shutdown system.
        reporter.printNotice(STRING_TRY_GJDOC_HELP);
        return false;
      }

      if (!rootDoc.hasSpecifiedPackagesOrClasses()) {
        reporter.printError("No packages or classes specified.");
        reporter.printNotice(STRING_TRY_GJDOC_HELP);
        return false;
      }

      rootDoc.setOptions(customOptionArr);

      rootDoc.build();

      //--- Bail out if no classes found

      if (0 == rootDoc.classes().length
          && 0 == rootDoc.specifiedPackages().length
          && 0 == rootDoc.specifiedClasses().length)
      {
        reporter.printError("No packages or classes found(!).");
        return false;
      }

      //--- Our work is done, tidy up memory

      System.gc();
      System.gc();

      //--- Set flag indicating Phase II of documentation generation

      docletRunning = true;

      //--- Invoke the start method on the Doclet: produce output

      reporter.printNotice("Running doclet...");

      TemporaryStore tstore = new TemporaryStore(Main.rootDoc);

      Thread.currentThread().setContextClassLoader(docletClass.getClassLoader());

      if (null != startTempMethod)
      {
        startTempMethod.invoke(null, new Object[]
          { tstore });
      }
      else
      {
        startMethod.invoke(null, new Object[]
          { tstore.getAndClear() });
      }

      //--- Let the user know how many warnings/errors occured

      if (reporter.getWarningCount() > 0)
      {
        reporter.printNotice(reporter.getWarningCount() + " warnings");
      }

      if (reporter.getErrorCount() > 0)
      {
        reporter.printNotice(reporter.getErrorCount() + " errors");
      }

      System.gc();

      //--- Done.
      return true;
    }
    catch (Exception e)
    {
      e.printStackTrace();
      return false;
    }
  }

  private void addFoundPackages(String subpackage, Set<String> foundPackages)
  {
    if (foundPackages.isEmpty()) {
      reporter.printWarning("No classes found under subpackage " + subpackage);
    }
    else {
      boolean onePackageAdded = false;
      for (Iterator<String> rit = foundPackages.iterator(); rit.hasNext();) {
        String foundPackage = rit.next();
        boolean excludeThisPackage = false;

        for (Iterator<String> eit = option_exclude.iterator(); eit.hasNext();) {
          String excludePackage = eit.next();
          if (foundPackage.equals(excludePackage) ||
              foundPackage.startsWith(excludePackage + ":")) {
            excludeThisPackage = true;
            break;
          }
        }

        if (!excludeThisPackage) {
          rootDoc.addSpecifiedPackageName(foundPackage);
          onePackageAdded = true;
        }
      }
      if (!onePackageAdded) {
        if (null != subpackage) {
          reporter.printWarning("No non-excluded classes found under subpackage " + subpackage);
        }
        else {
          reporter.printWarning("No non-excluded classes found.");
        }
      }
    }
  }

  /**
   *  Verify that the given file is a valid Java source file and that
   *  it specifies the given package.
   */
  private boolean isValidJavaFile(File file,
                                  String expectedPackage)
  {
    try {
      InputStream in = new BufferedInputStream(new FileInputStream(file));

      int ch, prevChar = 0;

      final int STATE_DEFAULT = 0;
      final int STATE_COMMENT = 1;
      final int STATE_LINE_COMMENT = 2;

      int state = STATE_DEFAULT;

      StringBuffer word = new StringBuffer();
      int wordIndex = 0;

      while ((ch = in.read()) >= 0) {
        String completeWord = null;

        switch (state) {
        case STATE_COMMENT:
          if (prevChar == '*' && ch == '/') {
            state = STATE_DEFAULT;
          }
          break;

        case STATE_LINE_COMMENT:
          if (ch == '\n') {
            state = STATE_DEFAULT;
          }
          break;

        case STATE_DEFAULT:
          if (prevChar == '/' && ch == '*') {
            word.deleteCharAt(word.length() - 1);
            if (word.length() > 0) {
              completeWord = word.toString();
              word.setLength(0);
            }
            state = STATE_COMMENT;
          }
          else if (prevChar == '/' && ch == '/') {
            word.deleteCharAt(word.length() - 1);
            if (word.length() > 0) {
              completeWord = word.toString();
              word.setLength(0);
            }
            state = STATE_LINE_COMMENT;
          }
          else if (" \t\r\n".indexOf(ch) >= 0) {
            if (word.length() > 0) {
              completeWord = word.toString();
              word.setLength(0);
            }
          }
          else if (1 == wordIndex && ';' == ch) {
            if (word.length() > 0) {
              completeWord = word.toString();
              word.setLength(0);
            }
            else {
              // empty package name in source file: "package ;" -> invalid source file
              in.close();
              return false;
            }
          }
          else {
            word.append((char)ch);
          }
          break;
        }

        if (null != completeWord) {
          if (0 == wordIndex && !"package".equals(completeWord)) {
            in.close();
            return "".equals(expectedPackage);
          }
          else if (1 == wordIndex) {
            in.close();
            return expectedPackage.equals(completeWord);
          }
          ++ wordIndex;
        }

        prevChar = ch;
      }

      // no package or class found before end-of-file -> invalid source file

      in.close();
      return false;
    }
    catch (IOException e) {
      reporter.printWarning("Could not examine file " + file + ": " + e);
      return false;
    }
  }

  /**
   *  Recursively try to locate valid Java packages under the given
   *  package specified by its name and its directory. Add the names
   *  of all valid packages to the result list.
   */
  private void findPackages(String subpackage,
                            File packageDir,
                            Set<String> result)
  {
    File[] files = packageDir.listFiles();
    if (null != files) {
      for (int i=0; i<files.length; ++i) {
        File file = files[i];
        if (!file.isDirectory() && file.getName().endsWith(".java")) {
          if (isValidJavaFile(file, subpackage)) {
            if ("".equals(subpackage)) {
              result.add(null);
            }
            else {
              result.add(subpackage);
            }
            break;
          }
        }
      }
      for (int i=0; i<files.length; ++i) {
        File file = files[i];
        if (file.isDirectory()) {
          String newSubpackage;
          if (null != subpackage && subpackage.length() > 0) {
            newSubpackage = subpackage + "." + file.getName();
          }
          else {
            newSubpackage = file.getName();
          }
          findPackages(newSubpackage, file, result);
        }
      }
    }
  }

  /**
   *
   */
  private static boolean validOptions(String options[][],
      DocErrorReporter reporter)
  {

    boolean foundDocletOption = false;
    for (int i = 0; i < options.length; i++)
    {
      String[] opt = options[i];
      if (opt[0].equalsIgnoreCase("-doclet"))
      {
        if (foundDocletOption)
        {
          reporter.printError("Only one -doclet option allowed.");
          return false;
        }
        else
        {
          foundDocletOption = true;
        }
      }
    }

    return true;
  }

  /**
   * Main entry point. This is the method called when gjdoc is invoked from the
   * command line.
   *
   * @param args
   *          command line arguments
   */
  public static void main(String[] args)
  {

    try
    {
      //--- Remember current time for profiling purposes

      Timer.setStartTime();

      //--- Handle control to the Singleton instance of this class

      int result = instance.start(args);

      if (result < 0) {
        // fatal error
        System.exit(5);
      }
      else if (result > 0) {
        // errors encountered
        System.exit(1);
      }
      else {
        // success
        System.exit(0);
      }
    }
    catch (Exception e)
    {
      //--- unexpected error
      e.printStackTrace();
      System.exit(1);
    }
  }

  /**
   * Parses command line arguments and subsequentially handles control to the
   * startDoclet() method
   *
   * @param args The command line parameters.
   */
   public static int execute(String[] args)
   {
     try
     {
       int result = instance.start(args);
       if (result < 0) {
         // fatal error
         return 5;
       }
       else if (result > 0) {
         // errors encountered
         return 1;
       }
       else {
         // success
         return 0;
       }
     }
     catch (Exception e)
     {
       // unexpected error
       return 1;
     }
   }

  /**
   * @param programName Name of the program (for error messages). *disregarded*
   * @param args The command line parameters.
   * @returns The return code.
   */
  public static int execute(String programName,
                            String[] args)
  {
    return execute(args);
  }

  /**
   * @param programName Name of the program (for error messages).
   * @param defaultDocletClassName Fully qualified class name.
   * @param args The command line parameters.
   * @returns The return code.
   *//*
  public static int execute(String programName,
                            String defaultDocletClassName,
                            String[] args)
  {
    // not yet implemented
  }*/

  /**
   * @param programName Name of the program (for error messages).
   * @param defaultDocletClassName Fully qualified class name.
   * @param args The command line parameters.
   * @returns The return code.
   *//*
  public static int execute(String programName,
                            String defaultDocletClassName,
                            String[] args)
  {
    // not yet implemented
  }*/

  /**
   * @param programName Name of the program (for error messages).
   * @param errWriter PrintWriter to receive error messages.
   * @param warnWriter PrintWriter to receive error messages.
   * @param noticeWriter PrintWriter to receive error messages.
   * @param defaultDocletClassName Fully qualified class name.
   * @param args The command line parameters.
   * @returns The return code.
   *//*
  public static int execute(String programName,
                            PrintWriter errWriter,
                            PrintWriter warnWriter,
                            PrintWriter noticeWriter,
                            String defaultDocletClassName,
                            String[] args)
  {
    // not yet implemented
  }*/

  /**
   * Parses command line arguments and subsequentially handles control to the
   * startDoclet() method
   *
   * @param args
   *          Command line arguments, as passed to the main() method
   * @return {@code -1} in case of a fatal error (invalid arguments),
   * or the number of errors encountered.
   * @exception ParseException
   *              FIXME
   * @exception IOException
   *              if an IO problem occur
   */
  public int start(String[] args) throws ParseException, IOException
  {

    //--- Collect unparsed arguments in array and resolve references
    //         to external argument files.

    List<String> arguments = new ArrayList<String>(args.length);

    for (int i = 0; i < args.length; ++i)
    {
      if (!args[i].startsWith("@"))
      {
        arguments.add(args[i]);
      }
      else
      {
        FileReader reader = new FileReader(args[i].substring(1));
        StreamTokenizer st = new StreamTokenizer(reader);
        st.resetSyntax();
        st.wordChars('\u0000', '\uffff');
        st.quoteChar('\"');
        st.quoteChar('\'');
        st.whitespaceChars(' ', ' ');
        st.whitespaceChars('\t', '\t');
        st.whitespaceChars('\r', '\r');
        st.whitespaceChars('\n', '\n');
        while (st.nextToken() != StreamTokenizer.TT_EOF)
        {
          arguments.add(st.sval);
        }
      }
    }

    //--- Initialize Map for option parsing

    initOptions();

    //--- This will hold all options recognized by gjdoc itself
    //         and their associated arguments.
    //         Contains objects of type String[], where each entry
    //         specifies an option along with its aguments.

    List<String[]> options = new LinkedList<String[]>();

    //--- This will hold all command line tokens not recognized
    //         to be part of a standard option.
    //         These options are intended to be processed by the doclet
    //         Contains objects of type String, where each entry is
    //         one unrecognized token.

    List<String> customOptions = new LinkedList<String>();

    rootDoc = new RootDocImpl();
    reporter = rootDoc.getReporter();

    //--- Iterate over all options given on the command line

    for (Iterator<String> it = arguments.iterator(); it.hasNext();)
    {

      String arg = it.next();

      //--- Check if gjdoc recognizes this option as a standard option
      //         and remember the options' argument count

      int optlen = optionLength(arg);

      //--- Argument count == 0 indicates that the option is not recognized.
      //         Add it to the list of custom option tokens

      //--- Otherwise the option is recognized as a standard option.
      //         if all required arguments are supplied. Create a new String
      //         array for the option and its arguments, and store it
      //         in the options array.

      if (optlen > 0)
      {
        String[] option = new String[optlen];
        option[0] = arg;
        boolean optargs_ok = true;
        for (int j = 1; j < optlen && optargs_ok; ++j)
        {
          if (it.hasNext())
          {
            option[j] = (String) it.next();
            if (option[j].startsWith("-"))
            {
              optargs_ok = false;
            }
          }
          else
          {
            optargs_ok = false;
          }
        }
        if (optargs_ok)
          options.add(option);
        else
        {
          //         If the option requires more arguments than given on the
          //         command line, issue a fatal error

          reporter.printFatal("Missing value for option " + arg + ".");
        }
      }
    }

    //--- Create an array of String arrays from the dynamic array built above

    String[][] optionArr = options.toArray(new String[options.size()][0]);

    //--- Validate all options and issue warnings/errors

    if (validOptions(optionArr, rootDoc))
    {

      //--- We got valid options; parse them and store the parsed values
      //         in 'option_*' fields.

      readOptions(optionArr);

      //--- Show version and exit if requested by user

      if (option_showVersion) {
        System.out.println("gjdoc " + getGjdocVersion());
        System.exit(0);
      }

      if (option_bootclasspath_specified) {
        reporter.printWarning("-bootclasspath ignored: not supported by"
                              + " gjdoc wrapper script, or no wrapper script in use.");
      }

      // If we have an empty source path list, add the current directory ('.')

      if (option_sourcepath.size() == 0)
        option_sourcepath.add(new File("."));

      //--- We have all information we need to start the doclet at this time

      if (null != option_encoding) {
        rootDoc.setSourceEncoding(option_encoding);
      }
      else {
        // be quiet about this for now:
        // reporter.printNotice("No encoding specified, using platform default: " + System.getProperty("file.encoding"));
        rootDoc.setSourceEncoding(System.getProperty("file.encoding"));
      }
      rootDoc.setSourcePath(option_sourcepath);

      //addJavaLangClasses();

      if (!startDoclet(arguments)) {
        return -1;
      }
    }

    return reporter.getErrorCount();
  }

  /*
  private void addJavaLangClasses()
    throws IOException
  {
    String resourceName = "/java.lang-classes-" + option_source + ".txt";
    InputStream in = getClass().getResourceAsStream(resourceName);
    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
    String line;
    while ((line = reader.readLine()) != null) {

      String className = line.trim();
      if (className.length() > 0) {
        ClassDocImpl classDoc =
          new ClassDocImpl(null, new PackageDocImpl("java.lang"),
                           ProgramElementDocImpl.ACCESS_PUBLIC,
                           false, false, null);
        classDoc.setClass(className);
        rootDoc.addClassDoc(classDoc);
      }
    }
  }
  */

  /**
   * Helper class for parsing command line arguments. An instance of this class
   * represents a particular option accepted by gjdoc (e.g. '-sourcepath') along
   * with the number of expected arguments and behavior to parse the arguments.
   */
  private abstract class OptionProcessor
  {

    /**
     * Number of arguments expected by this option.
     */
    private int argCount;

    /**
     * Initializes this instance.
     *
     * @param argCount
     *          number of arguments
     */
    public OptionProcessor(int argCount)
    {
      this.argCount = argCount;
    }

    /**
     * Overridden by derived classes with behavior to parse the arguments
     * specified with this option.
     *
     * @param args
     *          command line arguments
     */
    abstract void process(String[] args);
  }

  /**
   * Maps option tags (e.g. '-sourcepath') to OptionProcessor objects.
   * Initialized only once by method initOptions(). FIXME: Rename to
   * 'optionProcessors'.
   */
  private static Map<String,OptionProcessor> options = null;

  /**
   * Initialize all OptionProcessor objects needed to scan/parse command line
   * options. This cannot be done in a static initializer block because
   * OptionProcessors need access to the Singleton instance of the Main class.
   */
  private void initOptions()
  {

    options = new HashMap<String,OptionProcessor>();

    //--- Put one OptionProcessor object into the map
    //         for each option recognized.

    options.put("-overview", new OptionProcessor(2)
      {

        void process(String[] args)
        {
          System.err.println("WARNING: Unsupported option -overview ignored");
        }
      });
    options.put("-public", new OptionProcessor(1)
      {

        void process(String[] args)
        {
          option_coverage = COVERAGE_PUBLIC;
        }
      });
    options.put("-protected", new OptionProcessor(1)
      {

        void process(String[] args)
        {
          option_coverage = COVERAGE_PROTECTED;
        }
      });
    options.put("-package", new OptionProcessor(1)
      {

        void process(String[] args)
        {
          option_coverage = COVERAGE_PACKAGE;
        }
      });
    options.put("-private", new OptionProcessor(1)
      {

        void process(String[] args)
        {
          option_coverage = COVERAGE_PRIVATE;
        }
      });
    OptionProcessor helpProcessor = new OptionProcessor(1)
      {

        void process(String[] args)
        {
          option_help = true;
        }
      };

    options.put("-help", helpProcessor);
    options.put("--help", helpProcessor);
    options.put("-doclet", new OptionProcessor(2)
        {

          void process(String[] args)
          {
            option_doclet = args[0];
          }
        });
    options.put("-docletpath", new OptionProcessor(2)
        {

          void process(String[] args)
          {
            option_docletpath = args[0];
          }
        });
    options.put("-nowarn", new OptionProcessor(1)
        {

          void process(String[] args)
          {
            System.err.println("WARNING: Unsupported option -nowarn ignored");
          }
        });
    options.put("-source", new OptionProcessor(2)
        {

          void process(String[] args)
          {
            option_source = args[0];
            if ("1.5".equals(option_source)
                || "1.6".equals(option_source)
                || "1.7".equals(option_source)) {
              System.err.println("WARNING: support for option -source " + option_source + " is experimental");
            }
            else if (!"1.2".equals(option_source)
                && !"1.3".equals(option_source)
                && !"1.4".equals(option_source)) {

              throw new RuntimeException("Only the following values are currently"
                                         + " supported for option -source: 1.2, 1.3, 1.4; experimental: 1.5, 1.6, 1.7.");
            }
          }
        });
    OptionProcessor sourcePathProcessor = new OptionProcessor(2) {
        void process(String[] args)
        {
          Debug.log(1, "-sourcepath is '" + args[0] + "'");
          for (StringTokenizer st = new StringTokenizer(args[0],
              File.pathSeparator); st.hasMoreTokens();)
          {
            String path = st.nextToken();
            File file = new File(path);
            if (!(file.exists()))
            {
              throw new RuntimeException("The source path " + path
                  + " does not exist.");
            }
            option_sourcepath.add(file);
          }
        }
      };
    options.put("-s", sourcePathProcessor);
    options.put("-sourcepath", sourcePathProcessor);
    options.put("-subpackages", new OptionProcessor(2)
      {
        void process(String[] args)
        {
          StringTokenizer st = new StringTokenizer(args[0], ":");
          while (st.hasMoreTokens()) {
            String packageName = st.nextToken();

            if (packageName.startsWith(".")
                || packageName.endsWith(".")
                || packageName.indexOf("..") > 0
                || !checkCharSet(packageName,
                                 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_.")) {
              throw new RuntimeException("Illegal package name '"
                                         + packageName + "'");
            }
            option_subpackages.add(packageName);
          }
        }
      });
    options.put("-exclude", new OptionProcessor(2)
      {
        void process(String[] args)
        {
          StringTokenizer st = new StringTokenizer(args[0], ":");
          while (st.hasMoreTokens()) {
            String packageName = st.nextToken();

            if (packageName.startsWith(".")
                || packageName.endsWith(".")
                || packageName.indexOf("..") > 0
                || !checkCharSet(packageName,
                                 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_.")) {
              throw new RuntimeException("Illegal package name '"
                                         + packageName + "'");
            }
            option_exclude.add(packageName);
          }
        }
      });
    // TODO include other options here
    options.put("-verbose", new OptionProcessor(1)
      {

        void process(String[] args)
        {
          System.err.println("WARNING: Unsupported option -verbose ignored");
        }
      });
    options.put("-quiet", new OptionProcessor(1)
      {

        void process(String[] args)
        {
          reporter.setQuiet(true);
        }
      });
    options.put("-locale", new OptionProcessor(2)
      {

        void process(String[] args)
        {
          String localeName = args[0];
          String language = null;
          String country = null;
          String variant = null;
          StringTokenizer st = new StringTokenizer(localeName, "_");
          if (st.hasMoreTokens()) {
            language = st.nextToken();
          }
          if (st.hasMoreTokens()) {
            country = st.nextToken();
          }
          if (st.hasMoreTokens()) {
            variant = st.nextToken();
          }
          if (variant != null) {
            option_locale = new Locale(language, country, variant);
          }
          else if (country != null) {
             option_locale = new Locale(language, country);
          }
          else if (language != null) {
             option_locale = new Locale(language);
          }
          else {
              throw new RuntimeException("Illegal locale specification '"
                                         + localeName + "'");
          }
        }
      });
    options.put("-encoding", new OptionProcessor(2)
      {

        void process(String[] args)
        {
          option_encoding = args[0];
        }
      });
    options.put("-breakiterator", new OptionProcessor(1)
      {
        void process(String[] args)
        {
          option_breakiterator = true;
        }
      });
    options.put("-licensetext", new OptionProcessor(1)
      {
        void process(String[] args)
        {
          option_licensetext = true;
        }
      });
    options.put("-overview", new OptionProcessor(2)
      {
        void process(String[] args)
        {
          try {
            getRootDoc().setRawCommentText(RootDocImpl.readHtmlBody(new File(args[0])));
          }
          catch (IOException e) {
            throw new RuntimeException("Cannot read file specified in option -overview: " + e.getMessage());
          }
        }
      });
    options.put("-classpath", new OptionProcessor(2)
      {
        void process(String[] args)
        {
          reporter.printWarning("-classpath option could not be passed to the VM.  Faking it with ");
          reporter.printWarning("    System.setProperty(\"java.class.path\", \"" + args[0] + "\");");
          System.setProperty("java.class.path", args[0]);
        }
      });
    options.put("--version", new OptionProcessor(1)
      {
        void process(String[] args)
        {
          option_showVersion = true;
        }
      });
    options.put("-bootclasspath", new OptionProcessor(1)
      {
        void process(String[] args)
        {
          option_bootclasspath_specified = true;
        }
      });
    options.put("-all", new OptionProcessor(1)
      {
        void process(String[] args)
        {
          option_all = true;
        }
      });
    options.put("-reflection", new OptionProcessor(1)
      {
        void process(String[] args)
        {
          option_reflection = true;
        }
      });
  }

  /**
   * Determine how many arguments the given option requires.
   *
   * @param option
   *          The name of the option without leading dash.
   */
  private static int optionLength(String option)
  {

    OptionProcessor op = (OptionProcessor) options.get(option.toLowerCase());
    if (op != null)
      return op.argCount;
    else
      return 0;
  }

  /**
   * Process all given options. Assumes that the options have been validated
   * before.
   *
   * @param optionArr
   *          Each element is a series of Strings where [0] is the name of the
   *          option and [1..n] are the arguments to the option.
   */
  private void readOptions(String[][] optionArr)
  {

    //--- For each option, find the appropriate OptionProcessor
    //        and call its process() method

    for (int i = 0; i < optionArr.length; ++i)
    {
      String[] opt = optionArr[i];
      String[] args = new String[opt.length - 1];
      System.arraycopy(opt, 1, args, 0, opt.length - 1);
      OptionProcessor op = (OptionProcessor) options.get(opt[0].toLowerCase());
      op.process(args);
    }
  }

  /**
   * Print command line usage.
   */
  private static void usage()
  {
    System.out
        .print("\n"
            + "USAGE: gjdoc [options] [packagenames] "
            + "[sourcefiles] [@files]\n\n"
            + "  --version                Show version information and exit\n"
            + "  -all                     Process all source files found in the source path\n"
            + "  -overview <file>         Read overview documentation from HTML file\n"
            + "  -public                  Include only public classes and members\n"
            + "  -protected               Include protected and public classes and members\n"
            + "                           This is the default\n"
            + "  -package                 Include package/protected/public classes and members\n"
            + "  -private                 Include all classes and members\n"
            + "  -help, --help            Show this information\n"
            + "  -doclet <class>          Doclet class to use for generating output\n"
            + "  -docletpath <classpath>  Specifies the search path for the doclet and\n"
            + "                           dependencies\n"
            + "  -source <release>        Provide source compatibility with specified\n"
            + "                           release (1.4 to handle assertion)\n"
            + "  -sourcepath <pathlist>   Where to look for source files\n"
            + "  -s <pathlist>            Alias for -sourcepath\n"
            + "  -subpackages <spkglist>  List of subpackages to recursively load\n"
            + "  -exclude <pkglist>       List of packages to exclude\n"
            + "  -verbose                 Output messages about what Gjdoc is doing [ignored]\n"
            + "  -quiet                   Do not print non-error and non-warning messages\n"
            + "  -locale <name>           Locale to be used, e.g. en_US or en_US_WIN\n"
            + "  -encoding <name>         Source file encoding name\n"
            + "  -breakiterator           Compute first sentence with BreakIterator\n"
            + "  -classpath <pathlist>    Set the path used for loading auxilliary classes\n"
            + "\n"
            + "Standard doclet options:\n"
            + "  -d                      Set target directory\n"
            + "  -use                    Includes the 'Use' page for each documented class\n"
            + "                          and package\n"
            + "  -version                Includes the '@version' tag\n"
            + "  -author                 Includes the '@author' tag\n"
            + "  -splitindex             Splits the index file into multiple files\n"
            + "  -windowtitle <text>     Browser window title\n"
            + "  -doctitle <text>        Title near the top of the overview summary file\n"
            + "                          (HTML allowed)\n"
            + "  -title <text>           Title for this set of API documentation\n"
            + "                          (deprecated, -doctitle should be used instead)\n"
            + "  -header <text>          Text to include in the top navigation bar\n"
            + "                          (HTML allowed)\n"
            + "  -footer <text>          Text to include in the bottom navigation bar\n"
            + "                          (HTML allowed)\n"
            + "  -bottom <text>          Text to include at the bottom of each output file\n"
            + "                          (HTML allowed)\n"
            + "  -link <extdoc URL>      Link to external generated documentation at URL\n"
            + "  -linkoffline <extdoc URL> <packagelistLoc>\n"
            + "                          Link to external generated documentation for\n"
            + "                          the specified package-list\n"
            + "  -linksource             Creates an HTML version of each source file\n"
            + "  -group <groupheading> <packagepattern:packagepattern:...>\n"
            + "                          Separates packages on the overview page into groups\n"
            + "  -nodeprecated           Prevents the generation of any deprecated API\n"
            + "  -nodeprecatedlist       Prevents the generation of the file containing\n"
            + "                          the list of deprecated APIs and the link to the\n"
            + "                          navigation bar to that page\n"
            + "  -nosince                Omit the '@since' tag\n"
            + "  -notree                 Do not generate the class/interface hierarchy page\n"
            + "  -noindex                Do not generate the index file\n"
            + "  -nohelp                 Do not generate the help link\n"
            + "  -nonavbar               Do not generate the navbar, header and footer\n"
            + "  -helpfile <filen>       Path to an alternate help file\n"
            + "  -stylesheetfile <file>  Path to an alternate CSS stylesheet\n"
            + "  -addstylesheet <file>   Path to an additional CSS stylesheet\n"
            + "  -serialwarn             Complain about missing '@serial' tags [ignored]\n"
            + "  -charset <IANACharset>  Specifies the HTML charset\n"
            + "  -docencoding <IANACharset>\n"
            + "                          Specifies the encoding of the generated HTML files\n"
            + "  -tag <tagname>:Xaoptcmf:\"<taghead>\"\n"
            + "                          Enables gjdoc to interpret a custom tag\n"
            + "  -taglet                 Adds a Taglet class to the map of taglets\n"
            + "  -tagletpath             Sets the CLASSPATH to load subsequent Taglets from\n"
            + "  -docfilessubdirs        Enables deep copy of 'doc-files' directories\n"
            + "  -excludedocfilessubdir <name1:name2:...>\n"
            + "                          Excludes 'doc-files' subdirectories with a give name\n"
            + "  -noqualifier all|<packagename1:packagename2:...>\n"
            + "                          Do never fully qualify given package names\n"
            + "  -nocomment              Suppress the entire comment body including the main\n"
            + "                          description and all tags, only generate declarations\n"
            + "\n"
            + "Gjdoc extension options:\n"
            + "  -reflection             Use reflection for resolving unqualified class names\n"
            + "  -licensetext            Include license text from source files\n"
            + "  -validhtml              Use valid HTML/XML names (breaks compatibility)\n"
            + "  -baseurl <url>          Hardwire the given base URL into generated pages\n"
               /**
            + "  -genhtml                Generate HTML code instead of XML code. This is the\n"
            + "                          default.\n"
            + "  -geninfo                Generate Info code instead of XML code.\n"
            + "  -xslsheet <file>        If specified, XML files will be written to a\n"
            + "                          temporary directory and transformed using the\n"
            + "                          given XSL sheet. The result of the transformation\n"
            + "                          is written to the output directory. Not required if\n"
            + "                          -genhtml or -geninfo has been specified.\n"
            + "  -xmlonly                Generate XML code only, do not generate HTML code.\n"
            + "  -bottomnote             HTML code to include at the bottom of each page.\n"
            + "  -nofixhtml              If not specified, heurestics will be applied to\n"
            + "                          fix broken HTML code in comments.\n"
            + "  -nohtmlwarn             Do not emit warnings when encountering broken HTML\n"
            + "                          code.\n"
            + "  -noemailwarn            Do not emit warnings when encountering strings like\n"
            + "                          <abc@foo.com>.\n"
            + "  -indentstep <n>         How many spaces to indent each tag level in\n"
            + "                          generated XML code.\n"
            + "  -xsltdriver <class>     Specifies the XSLT driver to use for transformation.\n"
            + "                          By default, xsltproc is used.\n"
            + "  -postprocess <class>    XmlDoclet postprocessor class to apply after XSL\n"
            + "                          transformation.\n"
            + "  -compress               Generated info pages will be Zip-compressed.\n"
            + "  -workpath               Specify a temporary directory to use.\n"
            + "  -authormail <type>      Specify handling of mail addresses in @author tags.\n"
            + "     no-replace             do not replace mail addresses (default).\n"
            + "     mailto-name            replace by <a>Real Name</a>.\n"
            + "     name-mailto-address    replace by Real Name (<a>abc@foo.com</a>).\n"
            + "     name-mangled-address   replace by Real Name (<a>abc AT foo DOT com</a>).\n"
               **/
            );
  }

  /**
   * The root of the gjdoc tool.
   *
   * @return all the options of the gjdoc application.
   */
  public static RootDocImpl getRootDoc()
  {
    return rootDoc;
  }

  /**
   * Get the gjdoc singleton.
   *
   * @return the gjdoc instance.
   */
  public static Main getInstance()
  {
    return instance;
  }

  /**
   * Is this access level covered?
   *
   * @param accessLevel
   *          the access level we want to know if it is covered.
   * @return true if the access level is covered.
   */
  public boolean includeAccessLevel(int accessLevel)
  {
    return coverageTemplates[option_coverage][accessLevel];
  }

  /**
   * Is the doclet running?
   *
   * @return true if it's running
   */
  public boolean isDocletRunning()
  {
    return docletRunning;
  }

  /**
   * Check the charset. Check that all the characters of the string 'toCheck'
   * and query if they exist in the 'charSet'. The order does not matter. The
   * number of times a character is in the variable does not matter.
   *
   * @param toCheck
   *          the charset to check.
   * @param charSet
   *          the reference charset
   * @return true if they match.
   */
  public static boolean checkCharSet(String toCheck, String charSet)
  {
    for (int i = 0; i < toCheck.length(); ++i)
    {
      if (charSet.indexOf(toCheck.charAt(i)) < 0)
        return false;
    }
    return true;
  }

  /**
   * Makes the RootDoc eligible for the GC.
   */
  public static void releaseRootDoc()
  {
    rootDoc.flush();
  }

  /**
   * Return whether the -breakiterator option has been specified.
   */
  public boolean isUseBreakIterator()
  {
    return this.option_breakiterator
      || !getLocale().getLanguage().equals(Locale.ENGLISH.getLanguage());
  }

  /**
   * Return whether boilerplate license text should be copied.
   */
  public boolean isCopyLicenseText()
  {
    return this.option_licensetext;
  }

  /**
   *  Return the locale specified using the -locale option or the
   *  default locale;
   */
  public Locale getLocale()
  {
    return this.option_locale;
  }

  /**
   *  Return the collator to use based on the specified -locale
   *  option. If no collator can be found for the given locale, a
   *  warning is emitted and the default collator is used instead.
   */
  public Collator getCollator()
  {
    if (null == this.collator) {
      Locale locale = getLocale();
      this.collator = Collator.getInstance(locale);
      Locale defaultLocale = Locale.getDefault();
      if (null == this.collator
          && !defaultLocale.equals(locale)) {
        this.collator = Collator.getInstance(defaultLocale);
        if (null != this.collator) {
          reporter.printWarning("No collator found for locale "
                                + locale.getDisplayName()
                                + "; using collator for default locale "
                                + defaultLocale.getDisplayName()
                                + ".");
        }
        else {
          this.collator = Collator.getInstance();
          reporter.printWarning("No collator found for specified locale "
                                + locale.getDisplayName()
                                + " or default locale "
                                + defaultLocale.getDisplayName()
                                + ": using default collator.");
        }
      }
      if (null == this.collator) {
        this.collator = Collator.getInstance();
        reporter.printWarning("No collator found for locale "
                              + locale.getDisplayName()
                              + ": using default collator.");
      }
    }
    return this.collator;
  }

  public boolean isCacheRawComments()
  {
    return true;
  }

  public String getGjdocVersion()
  {
    if (null == gjdocVersion) {
      gjdocVersion = gnu.classpath.Configuration.CLASSPATH_VERSION;
    }
    return gjdocVersion;
  }

  public boolean isReflectionEnabled()
  {
    return this.option_reflection;
  }
}
