/*
 * Copyright (c) 2004, 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.apt.main;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;
import java.util.ResourceBundle;
import java.util.MissingResourceException;
import java.util.StringTokenizer;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;

import java.net.URLClassLoader;
import java.net.URL;
import java.net.MalformedURLException;

import javax.tools.JavaFileManager;
import javax.tools.StandardLocation;

import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.code.Source;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.jvm.Target;
import com.sun.tools.javac.util.*;

import com.sun.tools.apt.comp.AnnotationProcessingError;
import com.sun.tools.apt.comp.UsageMessageNeededException;
import com.sun.tools.apt.util.Bark;
import com.sun.mirror.apt.AnnotationProcessorFactory;

import static com.sun.tools.javac.file.Paths.pathToURLs;

/** This class provides a commandline interface to the apt build-time
 *  tool.
 *
 *  <p><b>This is NOT part of any supported API.
 *  If you write code that depends on this, you do so at your own
 *  risk.  This code and its internal interfaces are subject to change
 *  or deletion without notice.</b>
 */
@SuppressWarnings("deprecation")
public class Main {

    /** For testing: enter any options you want to be set implicitly
     *  here.
     */
    static String[] forcedOpts = {
        // Preserve parameter names from class files if the class was
        // compiled with debug enabled
        "-XDsave-parameter-names"
    };

    /** The name of the compiler, for use in diagnostics.
     */
    String ownName;

    /** The writer to use for diagnostic output.
     */
    PrintWriter out;


    /** Instantiated factory to use in lieu of discovery process.
     */
    AnnotationProcessorFactory providedFactory = null;

    /** Map representing original command-line arguments.
     */
    Map<String,String> origOptions = new HashMap<String, String>();

    /** Classloader to use for finding factories.
     */
    ClassLoader aptCL = null;

    /** Result codes.
     */
    static final int
        EXIT_OK = 0,        // Compilation completed with no errors.
        EXIT_ERROR = 1,     // Completed but reported errors.
        EXIT_CMDERR = 2,    // Bad command-line arguments
        EXIT_SYSERR = 3,    // System error or resource exhaustion.
        EXIT_ABNORMAL = 4;  // Compiler terminated abnormally

    /** This class represents an option recognized by the main program
     */
    private class Option {
        /** Whether or not the option is used only aptOnly.
         */
        boolean aptOnly = false;

        /** Option string.
         */
        String name;

        /** Documentation key for arguments.
         */
        String argsNameKey;

        /** Documentation key for description.
         */
        String descrKey;

        /** Suffix option (-foo=bar or -foo:bar)
         */
        boolean hasSuffix;

        Option(String name, String argsNameKey, String descrKey) {
            this.name = name;
            this.argsNameKey = argsNameKey;
            this.descrKey = descrKey;
            char lastChar = name.charAt(name.length()-1);
            hasSuffix = lastChar == ':' || lastChar == '=';
        }
        Option(String name, String descrKey) {
            this(name, null, descrKey);
        }

        public String toString() {
            return name;
        }

        /** Does this option take a (separate) operand?
         */
        boolean hasArg() {
            return argsNameKey != null && !hasSuffix;
        }

        /** Does argument string match option pattern?
         *  @param arg        The command line argument string.
         */
        boolean matches(String arg) {
            return hasSuffix ? arg.startsWith(name) : arg.equals(name);
        }

        /** For javac-only options, print nothing.
         */
        void help() {
        }

        String helpSynopsis() {
            return name +
                (argsNameKey == null ? "" :
                 ((hasSuffix ? "" : " ") +
                  getLocalizedString(argsNameKey)));
        }

        /** Print a line of documentation describing this option, if non-standard.
         */
        void xhelp() {}

        /** Process the option (with arg). Return true if error detected.
         */
        boolean process(String option, String arg) {
            options.put(option, arg);
            return false;
        }

        /** Process the option (without arg). Return true if error detected.
         */
        boolean process(String option) {
            if (hasSuffix)
                return process(name, option.substring(name.length()));
            else
                return process(option, option);
        }
    };

    private class SharedOption extends Option {
        SharedOption(String name, String argsNameKey, String descrKey) {
            super(name, argsNameKey, descrKey);
        }

        SharedOption(String name, String descrKey) {
            super(name, descrKey);
        }

        void help() {
            String s = "  " + helpSynopsis();
            out.print(s);
            for (int j = s.length(); j < 29; j++) out.print(" ");
            Bark.printLines(out, getLocalizedString(descrKey));
        }

    }

    private class AptOption extends Option {
        AptOption(String name, String argsNameKey, String descrKey) {
            super(name, argsNameKey, descrKey);
            aptOnly = true;
        }

        AptOption(String name, String descrKey) {
            super(name, descrKey);
            aptOnly = true;
        }

        /** Print a line of documentation describing this option, if standard.
         */
        void help() {
            String s = "  " + helpSynopsis();
            out.print(s);
            for (int j = s.length(); j < 29; j++) out.print(" ");
            Bark.printLines(out, getLocalizedString(descrKey));
        }

    }

    /** A nonstandard or extended (-X) option
     */
    private class XOption extends Option {
        XOption(String name, String argsNameKey, String descrKey) {
            super(name, argsNameKey, descrKey);
        }
        XOption(String name, String descrKey) {
            this(name, null, descrKey);
        }
        void help() {}
        void xhelp() {}
    };

    /** A nonstandard or extended (-X) option
     */
    private class AptXOption extends Option {
        AptXOption(String name, String argsNameKey, String descrKey) {
            super(name, argsNameKey, descrKey);
            aptOnly = true;
        }
        AptXOption(String name, String descrKey) {
            this(name, null, descrKey);
        }
        void xhelp() {
            String s = "  " + helpSynopsis();
            out.print(s);
            for (int j = s.length(); j < 29; j++) out.print(" ");
            Log.printLines(out, getLocalizedString(descrKey));
        }
    };

    /** A hidden (implementor) option
     */
    private class HiddenOption extends Option {
        HiddenOption(String name) {
            super(name, null, null);
        }
        HiddenOption(String name, String argsNameKey) {
            super(name, argsNameKey, null);
        }
        void help() {}
        void xhelp() {}
    };

    private class AptHiddenOption extends HiddenOption {
        AptHiddenOption(String name) {
            super(name);
            aptOnly = true;
        }
        AptHiddenOption(String name, String argsNameKey) {
            super(name, argsNameKey);
            aptOnly = true;
        }
    }

    private Option[] recognizedOptions = {
        new Option("-g",                                        "opt.g"),
        new Option("-g:none",                                   "opt.g.none") {
            boolean process(String option) {
                options.put("-g:", "none");
                return false;
            }
        },

        new Option("-g:{lines,vars,source}",                    "opt.g.lines.vars.source") {
            boolean matches(String s) {
                return s.startsWith("-g:");
            }
            boolean process(String option) {
                String suboptions = option.substring(3);
                options.put("-g:", suboptions);
                // enter all the -g suboptions as "-g:suboption"
                for (StringTokenizer t = new StringTokenizer(suboptions, ","); t.hasMoreTokens(); ) {
                    String tok = t.nextToken();
                    String opt = "-g:" + tok;
                    options.put(opt, opt);
                }
                return false;
            }
        },

        new XOption("-Xlint",                                   "opt.Xlint"),
        new XOption("-Xlint:{"
                    + "all,"
                    + "cast,deprecation,divzero,empty,unchecked,fallthrough,path,serial,finally,overrides,"
                    + "-cast,-deprecation,-divzero,-empty,-unchecked,-fallthrough,-path,-serial,-finally,-overrides,"
                    + "none}",
                                                                "opt.Xlint.suboptlist") {
            boolean matches(String s) {
                return s.startsWith("-Xlint:");
            }
            boolean process(String option) {
                String suboptions = option.substring(7);
                options.put("-Xlint:", suboptions);
                // enter all the -Xlint suboptions as "-Xlint:suboption"
                for (StringTokenizer t = new StringTokenizer(suboptions, ","); t.hasMoreTokens(); ) {
                    String tok = t.nextToken();
                    String opt = "-Xlint:" + tok;
                    options.put(opt, opt);
                }
                return false;
            }
        },

        new Option("-nowarn",                                   "opt.nowarn"),
        new Option("-verbose",                                  "opt.verbose"),

        // -deprecation is retained for command-line backward compatibility
        new Option("-deprecation",                              "opt.deprecation") {
                boolean process(String option) {
                    options.put("-Xlint:deprecation", option);
                    return false;
                }
            },

        new SharedOption("-classpath",     "opt.arg.path",      "opt.classpath"),
        new SharedOption("-cp",            "opt.arg.path",      "opt.classpath") {
            boolean process(String option, String arg) {
                return super.process("-classpath", arg);
            }
        },
        new Option("-sourcepath",          "opt.arg.path",      "opt.sourcepath"),
        new Option("-bootclasspath",       "opt.arg.path",      "opt.bootclasspath") {
            boolean process(String option, String arg) {
                options.remove("-Xbootclasspath/p:");
                options.remove("-Xbootclasspath/a:");
                return super.process(option, arg);
            }
        },
        new XOption("-Xbootclasspath/p:",  "opt.arg.path", "opt.Xbootclasspath.p"),
        new XOption("-Xbootclasspath/a:",  "opt.arg.path", "opt.Xbootclasspath.a"),
        new XOption("-Xbootclasspath:",    "opt.arg.path", "opt.bootclasspath") {
            boolean process(String option, String arg) {
                options.remove("-Xbootclasspath/p:");
                options.remove("-Xbootclasspath/a:");
                return super.process("-bootclasspath", arg);
            }
        },
        new Option("-extdirs",             "opt.arg.dirs",      "opt.extdirs"),
        new XOption("-Djava.ext.dirs=",    "opt.arg.dirs",      "opt.extdirs") {
            boolean process(String option, String arg) {
                return super.process("-extdirs", arg);
            }
        },
        new Option("-endorseddirs",        "opt.arg.dirs",      "opt.endorseddirs"),
        new XOption("-Djava.endorsed.dirs=","opt.arg.dirs",     "opt.endorseddirs") {
            boolean process(String option, String arg) {
                return super.process("-endorseddirs", arg);
            }
        },
        new Option("-proc:{none, only}",                        "opt.proc.none.only") {
            public boolean matches(String s) {
                return s.equals("-proc:none") || s.equals("-proc:only");
            }
        },
        new Option("-processor",        "opt.arg.class",        "opt.processor"),
        new Option("-processorpath",    "opt.arg.path",         "opt.processorpath"),

        new SharedOption("-d",          "opt.arg.path", "opt.d"),
        new SharedOption("-s",          "opt.arg.path", "opt.s"),
        new Option("-encoding",         "opt.arg.encoding",     "opt.encoding"),
        new SharedOption("-source",             "opt.arg.release",      "opt.source") {
            boolean process(String option, String operand) {
                Source source = Source.lookup(operand);
                if (source == null) {
                    error("err.invalid.source", operand);
                    return true;
                } else if (source.compareTo(Source.JDK1_5) > 0) {
                    error("err.unsupported.source.version", operand);
                    return true;
                }
                return super.process(option, operand);
            }
        },
        new Option("-target",           "opt.arg.release",      "opt.target") {
            boolean process(String option, String operand) {
                Target target = Target.lookup(operand);
                if (target == null) {
                    error("err.invalid.target", operand);
                    return true;
                } else if (target.compareTo(Target.JDK1_5) > 0) {
                    error("err.unsupported.target.version", operand);
                    return true;
                }
                return super.process(option, operand);
            }
        },
        new AptOption("-version",               "opt.version") {
            boolean process(String option) {
                Bark.printLines(out, ownName + " " + AptJavaCompiler.version());
                return super.process(option);
            }
        },
        new HiddenOption("-fullversion"),
        new AptOption("-help",                                  "opt.help") {
            boolean process(String option) {
                Main.this.help();
                return super.process(option);
            }
        },
        new SharedOption("-X",                                  "opt.X") {
            boolean process(String option) {
                Main.this.xhelp();
                return super.process(option);
            }
        },

        // This option exists only for the purpose of documenting itself.
        // It's actually implemented by the launcher.
        new AptOption("-J",             "opt.arg.flag",         "opt.J") {
            String helpSynopsis() {
                hasSuffix = true;
                return super.helpSynopsis();
            }
            boolean process(String option) {
                throw new AssertionError
                    ("the -J flag should be caught by the launcher.");
            }
        },


        new SharedOption("-A",          "opt.proc.flag",        "opt.A") {
                String helpSynopsis() {
                    hasSuffix = true;
                    return super.helpSynopsis();
                }

                boolean matches(String arg) {
                    return arg.startsWith("-A");
                }

                boolean hasArg() {
                    return false;
                }

                boolean process(String option) {
                    return process(option, option);
                }
            },

        new AptOption("-nocompile",     "opt.nocompile"),

        new AptOption("-print",         "opt.print"),

        new AptOption("-factorypath", "opt.arg.path", "opt.factorypath"),

        new AptOption("-factory",     "opt.arg.class", "opt.factory"),

        new AptXOption("-XListAnnotationTypes", "opt.XListAnnotationTypes"),

        new AptXOption("-XListDeclarations",    "opt.XListDeclarations"),

        new AptXOption("-XPrintAptRounds",      "opt.XPrintAptRounds"),

        new AptXOption("-XPrintFactoryInfo",    "opt.XPrintFactoryInfo"),

        /*
         * Option to treat both classes and source files as
         * declarations that can be given on the command line and
         * processed as the result of an apt round.
         */
        new AptXOption("-XclassesAsDecls", "opt.XClassesAsDecls"),

        // new Option("-moreinfo",                                      "opt.moreinfo") {
        new HiddenOption("-moreinfo") {
            boolean process(String option) {
                Type.moreInfo = true;
                return super.process(option);
            }
        },

        // treat warnings as errors
        new HiddenOption("-Werror"),

        // use complex inference from context in the position of a method call argument
        new HiddenOption("-complexinference"),

        // prompt after each error
        // new Option("-prompt",                                        "opt.prompt"),
        new HiddenOption("-prompt"),

        // dump stack on error
        new HiddenOption("-doe"),

        // display warnings for generic unchecked and unsafe operations
        new HiddenOption("-warnunchecked") {
            boolean process(String option) {
                options.put("-Xlint:unchecked", option);
                return false;
            }
        },

        new HiddenOption("-Xswitchcheck") {
            boolean process(String option) {
                options.put("-Xlint:switchcheck", option);
                return false;
            }
        },

        // generate trace output for subtyping operations
        new HiddenOption("-debugsubtyping"),

        new XOption("-Xmaxerrs",        "opt.arg.number",       "opt.maxerrs"),
        new XOption("-Xmaxwarns",       "opt.arg.number",       "opt.maxwarns"),
        new XOption("-Xstdout",         "opt.arg.file",         "opt.Xstdout") {
            boolean process(String option, String arg) {
                try {
                    out = new PrintWriter(new FileWriter(arg), true);
                } catch (java.io.IOException e) {
                    error("err.error.writing.file", arg, e);
                    return true;
                }
                return super.process(option, arg);
            }
        },

        new XOption("-Xprint",                                  "opt.print"),

        new XOption("-XprintRounds",                            "opt.printRounds"),

        new XOption("-XprintProcessorInfo",                     "opt.printProcessorInfo"),


        /* -O is a no-op, accepted for backward compatibility. */
        new HiddenOption("-O"),

        /* -Xjcov produces tables to support the code coverage tool jcov. */
        new HiddenOption("-Xjcov"),

        /* This is a back door to the compiler's option table.
         * -Dx=y sets the option x to the value y.
         * -Dx sets the option x to the value x.
         */
        new HiddenOption("-XD") {
            String s;
            boolean matches(String s) {
                this.s = s;
                return s.startsWith(name);
            }
            boolean process(String option) {
                s = s.substring(name.length());
                int eq = s.indexOf('=');
                String key = (eq < 0) ? s : s.substring(0, eq);
                String value = (eq < 0) ? s : s.substring(eq+1);
                options.put(key, value);
                return false;
            }
        },

        new HiddenOption("sourcefile") {
                String s;
                boolean matches(String s) {
                    this.s = s;
                    return s.endsWith(".java") ||
                        (options.get("-XclassesAsDecls") != null);
                }
                boolean process(String option) {
                    if (s.endsWith(".java")) {
                        if (!sourceFileNames.contains(s))
                            sourceFileNames.add(s);
                    } else if (options.get("-XclassesAsDecls") != null) {
                        classFileNames.add(s);
                    }
                    return false;
                }
            },
    };

    /**
     * Construct a compiler instance.
     */
    public Main(String name) {
        this(name, new PrintWriter(System.err, true));
    }

    /**
     * Construct a compiler instance.
     */
    public Main(String name, PrintWriter out) {
        this.ownName = name;
        this.out = out;
    }

    /** A table of all options that's passed to the JavaCompiler constructor.  */
    private Options options = null;

    /** The list of source files to process
     */
    java.util.List<String> sourceFileNames = new java.util.LinkedList<String>();

    /** The list of class files to process
     */
    java.util.List<String> classFileNames = new java.util.LinkedList<String>();

    /** List of top level names of generated source files from most recent apt round.
     */
    java.util.Set<String> genSourceFileNames = new java.util.LinkedHashSet<String>();

    /** List of names of generated class files from most recent apt round.
     */
    java.util.Set<String> genClassFileNames  = new java.util.LinkedHashSet<String>();

    /**
     * List of all the generated source file names across all apt rounds.
     */
    java.util.Set<String> aggregateGenSourceFileNames = new java.util.LinkedHashSet<String>();

    /**
     * List of all the generated class file names across all apt rounds.
     */
    java.util.Set<String> aggregateGenClassFileNames  = new java.util.LinkedHashSet<String>();

    /**
     * List of all the generated file names across all apt rounds.
     */
    java.util.Set<java.io.File> aggregateGenFiles = new java.util.LinkedHashSet<java.io.File>();

    /**
     * Set of all factories that have provided a processor on some apt round.
     */
    java.util.Set<Class<? extends AnnotationProcessorFactory> > productiveFactories  =
        new java.util.LinkedHashSet<Class<? extends AnnotationProcessorFactory> >();



    /** Print a string that explains usage.
     */
    void help() {
        Bark.printLines(out, getLocalizedString("msg.usage.header", ownName));
        for (int i=0; i < recognizedOptions.length; i++) {
            recognizedOptions[i].help();
        }
        Bark.printLines(out, getLocalizedString("msg.usage.footer"));
        out.println();
    }

    /** Print a string that explains usage for X options.
     */
    void xhelp() {
        for (int i=0; i<recognizedOptions.length; i++) {
            recognizedOptions[i].xhelp();
        }
        out.println();
        Bark.printLines(out, getLocalizedString("msg.usage.nonstandard.footer"));
    }

    /** Report a usage error.
     */
    void error(String key, Object... args) {
        warning(key, args);
        help();
    }

    /** Report a warning.
     */
    void warning(String key, Object... args) {
        Bark.printLines(out, ownName + ": "
                       + getLocalizedString(key, args));
    }

    /** Process command line arguments: store all command line options
     *  in `options' table and return all source filenames.
     *  @param args    The array of command line arguments.
     */
    protected java.util.List<String> processArgs(String[] flags) {
        int ac = 0;
        while (ac < flags.length) {
            String flag = flags[ac];
            ac++;

            int j;
            for (j=0; j < recognizedOptions.length; j++)
                if (recognizedOptions[j].matches(flag))
                    break;

            if (j == recognizedOptions.length) {
                error("err.invalid.flag", flag);
                return null;
            }

            Option option = recognizedOptions[j];
            if (option.hasArg()) {
                if (ac == flags.length) {
                    error("err.req.arg", flag);
                    return null;
                }
                String operand = flags[ac];
                ac++;
                if (option.process(flag, operand))
                    return null;
            } else {
                if (option.process(flag))
                    return null;
            }
        }

        String sourceString = options.get("-source");
        Source source = (sourceString != null)
            ? Source.lookup(sourceString)
            : Source.JDK1_5; // JDK 5 is the latest supported source version
        String targetString = options.get("-target");
        Target target = (targetString != null)
            ? Target.lookup(targetString)
            : Target.JDK1_5; // JDK 5 is the latest supported source version
        // We don't check source/target consistency for CLDC, as J2ME
        // profiles are not aligned with J2SE targets; moreover, a
        // single CLDC target may have many profiles.  In addition,
        // this is needed for the continued functioning of the JSR14
        // prototype.
        if (Character.isDigit(target.name.charAt(0)) &&
            target.compareTo(source.requiredTarget()) < 0) {
            if (targetString != null) {
                if (sourceString == null) {
                    warning("warn.target.default.source.conflict",
                            targetString,
                            source.requiredTarget().name);
                } else {
                    warning("warn.source.target.conflict",
                            sourceString,
                            source.requiredTarget().name);
                }
                return null;
            } else {
                options.put("-target", source.requiredTarget().name);
            }
        }
        return sourceFileNames;
    }

    /** Programmatic interface for main function.
     * @param args    The command line parameters.
     */
    public int compile(String[] args, AnnotationProcessorFactory factory) {
        int returnCode = 0;
        providedFactory = factory;

        Context context = new Context();
        JavacFileManager.preRegister(context);
        options = Options.instance(context);
        Bark bark;

        /*
         * Process the command line options to create the intial
         * options data.  This processing is at least partially reused
         * by any recursive apt calls.
         */

        // For testing: assume all arguments in forcedOpts are
        // prefixed to command line arguments.
        processArgs(forcedOpts);

        /*
         * A run of apt only gets passed the most recently generated
         * files; the initial run of apt gets passed the files from
         * the command line.
         */

        java.util.List<String> origFilenames;
        try {
            // assign args the result of parse to capture results of
            // '@file' expansion
            origFilenames = processArgs((args=CommandLine.parse(args)));

            if (options.get("suppress-tool-api-removal-message") == null) {
                Bark.printLines(out, getLocalizedString("misc.Deprecation"));
            }

            if (origFilenames == null) {
                return EXIT_CMDERR;
            } else if (origFilenames.size() == 0) {
                // it is allowed to compile nothing if just asking for help
                if (options.get("-help") != null ||
                    options.get("-X") != null)
                    return EXIT_OK;
            }
        } catch (java.io.FileNotFoundException e) {
            Bark.printLines(out, ownName + ": " +
                           getLocalizedString("err.file.not.found",
                                              e.getMessage()));
            return EXIT_SYSERR;
        } catch (IOException ex) {
            ioMessage(ex);
            return EXIT_SYSERR;
        } catch (OutOfMemoryError ex) {
            resourceMessage(ex);
            return EXIT_SYSERR;
        } catch (StackOverflowError ex) {
            resourceMessage(ex);
            return EXIT_SYSERR;
        } catch (FatalError ex) {
            feMessage(ex);
            return EXIT_SYSERR;
        } catch (sun.misc.ServiceConfigurationError sce) {
            sceMessage(sce);
            return EXIT_ABNORMAL;
        } catch (Throwable ex) {
            bugMessage(ex);
            return EXIT_ABNORMAL;
        }


        boolean firstRound = true;
        boolean needSourcePath = false;
        boolean needClassPath  = false;
        boolean classesAsDecls = options.get("-XclassesAsDecls") != null;

        /*
         * Create augumented classpath and sourcepath values.
         *
         * If any of the prior apt rounds generated any new source
         * files, the n'th apt round (and any javac invocation) has the
         * source destination path ("-s path") as the last element of
         * the "-sourcepath" to the n'th call.
         *
         * If any of the prior apt rounds generated any new class files,
         * the n'th apt round (and any javac invocation) has the class
         * destination path ("-d path") as the last element of the
         * "-classpath" to the n'th call.
         */
        String augmentedSourcePath = "";
        String augmentedClassPath = "";
        String baseClassPath = "";

        try {
            /*
             * Record original options for future annotation processor
             * invocations.
             */
            origOptions = new HashMap<String, String>(options.size());
            for(String s: options.keySet()) {
                String value;
                if (s.equals(value = options.get(s)))
                    origOptions.put(s, (String)null);
                else
                    origOptions.put(s, value);
            }
            origOptions = Collections.unmodifiableMap(origOptions);

            JavacFileManager fm = (JavacFileManager) context.get(JavaFileManager.class);
            {
                // Note: it might be necessary to check for an empty
                // component ("") of the source path or class path

                String sourceDest = options.get("-s");
                if (fm.hasLocation(StandardLocation.SOURCE_PATH)) {
                    for(File f: fm.getLocation(StandardLocation.SOURCE_PATH))
                        augmentedSourcePath += (f + File.pathSeparator);
                    augmentedSourcePath += (sourceDest == null)?".":sourceDest;
                } else {
                    augmentedSourcePath = ".";

                    if (sourceDest != null)
                        augmentedSourcePath += (File.pathSeparator + sourceDest);
                }

                String classDest = options.get("-d");
                if (fm.hasLocation(StandardLocation.CLASS_PATH)) {
                    for(File f: fm.getLocation(StandardLocation.CLASS_PATH))
                        baseClassPath += (f + File.pathSeparator);
                    // put baseClassPath into map to handle any
                    // value needed for the classloader
                    options.put("-classpath", baseClassPath);

                    augmentedClassPath = baseClassPath + ((classDest == null)?".":classDest);
                } else {
                    baseClassPath = ".";
                    if (classDest != null)
                        augmentedClassPath = baseClassPath + (File.pathSeparator + classDest);
                }
                assert options.get("-classpath") != null;
            }

            /*
             * Create base and augmented class loaders
             */
            ClassLoader augmentedAptCL = null;
            {
            /*
             * Use a url class loader to look for classes on the
             * user-specified class path. Prepend computed bootclass
             * path, which includes extdirs, to the URLClassLoader apt
             * uses.
             */
                String aptclasspath = "";
                String bcp = "";
                Iterable<? extends File> bootclasspath = fm.getLocation(StandardLocation.PLATFORM_CLASS_PATH);

                if (bootclasspath != null) {
                    for(File f: bootclasspath)
                        bcp += (f + File.pathSeparator);
                }

                // If the factory path is set, use that path
                if (providedFactory == null)
                    aptclasspath = options.get("-factorypath");
                if (aptclasspath == null)
                    aptclasspath = options.get("-classpath");

                assert aptclasspath != null;
                aptclasspath = (bcp + aptclasspath);
                aptCL = new URLClassLoader(pathToURLs(aptclasspath));

                if (providedFactory == null &&
                    options.get("-factorypath") != null) // same CL even if new class files written
                    augmentedAptCL = aptCL;
                else {
                    // Create class loader in case new class files are
                    // written
                    augmentedAptCL = new URLClassLoader(pathToURLs(augmentedClassPath.
                                                                   substring(baseClassPath.length())),
                                                        aptCL);
                }
            }

            int round = 0; // For -XPrintAptRounds
            do {
                round++;

                Context newContext = new Context();
                Options newOptions = Options.instance(newContext); // creates a new context
                newOptions.putAll(options);

                // populate with old options... don't bother reparsing command line, etc.

                // if genSource files, must add destination to source path
                if (genSourceFileNames.size() > 0 && !firstRound) {
                    newOptions.put("-sourcepath", augmentedSourcePath);
                    needSourcePath = true;
                }
                aggregateGenSourceFileNames.addAll(genSourceFileNames);
                sourceFileNames.addAll(genSourceFileNames);
                genSourceFileNames.clear();

                // Don't really need to track this; just have to add -d
                // "foo" to class path if any class files are generated
                if (genClassFileNames.size() > 0) {
                    newOptions.put("-classpath", augmentedClassPath);
                    aptCL = augmentedAptCL;
                    needClassPath = true;
                }
                aggregateGenClassFileNames.addAll(genClassFileNames);
                classFileNames.addAll(genClassFileNames);
                genClassFileNames.clear();

                options = newOptions;

                if (options.get("-XPrintAptRounds") != null) {
                    out.println("apt Round : " + round);
                    out.println("filenames: " + sourceFileNames);
                    if (classesAsDecls)
                        out.println("classnames: " + classFileNames);
                    out.println("options: " + options);
                }

                returnCode = compile(args, newContext);
                firstRound = false;

                // Check for reported errors before continuing
                bark = Bark.instance(newContext);
            } while(((genSourceFileNames.size() != 0 ) ||
                     (classesAsDecls && genClassFileNames.size() != 0)) &&
                    bark.nerrors == 0);
        } catch (UsageMessageNeededException umne) {
            help();
            return EXIT_CMDERR; // will cause usage message to be printed
        }

        /*
         * Do not compile if a processor has reported an error or if
         * there are no source files to process.  A more sophisticated
         * test would also fail for syntax errors caught by javac.
         */
        if (options.get("-nocompile") == null &&
            options.get("-print")     == null &&
            bark.nerrors == 0 &&
            (origFilenames.size() > 0 || aggregateGenSourceFileNames.size() > 0 )) {
            /*
             * Need to create new argument string for calling javac:
             * 1. apt specific arguments (e.g. -factory) must be stripped out
             * 2. proper settings for sourcepath and classpath must be used
             * 3. generated class names must be added
             * 4. class file names as declarations must be removed
             */

            int newArgsLength = args.length +
                (needSourcePath?1:0) +
                (needClassPath?1:0) +
                aggregateGenSourceFileNames.size();

            // Null out apt-specific options and don't copy over into
            // newArgs. This loop should be a lot faster; the options
            // array should be replaced with a better data structure
            // which includes a map from strings to options.
            //
            // If treating classes as declarations, must strip out
            // class names from the javac argument list
            argLoop:
            for(int i = 0; i < args.length; i++) {
                int matchPosition = -1;

                // "-A" by itself is recognized by apt but not javac
                if (args[i] != null && args[i].equals("-A")) {
                    newArgsLength--;
                    args[i] = null;
                    continue argLoop;
                } else {
                    optionLoop:
                    for(int j = 0; j < recognizedOptions.length; j++) {
                        if (args[i] != null && recognizedOptions[j].matches(args[i])) {
                            matchPosition = j;
                            break optionLoop;
                        }
                    }

                    if (matchPosition != -1) {
                        Option op = recognizedOptions[matchPosition];
                        if (op.aptOnly) {
                            newArgsLength--;
                            args[i] = null;
                            if (op.hasArg()) {
                                newArgsLength--;
                                args[i+1] = null;
                            }
                        } else {
                            if (op.hasArg()) { // skip over next string
                                i++;
                                continue argLoop;
                            }

                            if ((options.get("-XclassesAsDecls") != null) &&
                                (matchPosition == (recognizedOptions.length-1)) ){
                                // Remove class file names from
                                // consideration by javac.
                                if (! args[i].endsWith(".java")) {
                                    newArgsLength--;
                                    args[i] = null;
                                }
                            }
                        }
                    }
                }
            }

            String newArgs[] = new String[newArgsLength];

            int j = 0;
            for(int i=0; i < args.length; i++) {
                if (args[i] != null)
                    newArgs[j++] = args[i];
            }

            if (needClassPath)
                newArgs[j++] = "-XD-classpath=" + augmentedClassPath;

            if (needSourcePath) {
                newArgs[j++] = "-XD-sourcepath=" + augmentedSourcePath;

                for(String s: aggregateGenSourceFileNames)
                    newArgs[j++] = s;
            }

            returnCode = com.sun.tools.javac.Main.compile(newArgs);
        }

        return returnCode;
    }

    /** Programmatic interface for main function.
     * @param args    The command line parameters.
     */
    int compile(String[] args, Context context) {
        boolean assertionsEnabled = false;
        assert assertionsEnabled = true;
        if (!assertionsEnabled) {
            // Bark.printLines(out, "fatal error: assertions must be enabled when running javac");
            // return EXIT_ABNORMAL;
        }
        int exitCode = EXIT_OK;

        AptJavaCompiler comp = null;
        try {
            context.put(Bark.outKey, out);

            comp = AptJavaCompiler.instance(context);
            if (comp == null)
                return EXIT_SYSERR;

            java.util.List<String> nameList = new java.util.LinkedList<String>();
            nameList.addAll(sourceFileNames);
            if (options.get("-XclassesAsDecls") != null)
                nameList.addAll(classFileNames);

            List<Symbol.ClassSymbol> cs
                = comp.compile(List.from(nameList.toArray(new String[0])),
                               origOptions,
                               aptCL,
                               providedFactory,
                               productiveFactories,
                               aggregateGenFiles);

            /*
             * If there aren't new source files, we shouldn't bother
             *  running javac if there were errors.
             *
             * If there are new files, we should try running javac in
             * case there were typing errors.
             *
             */

            if (comp.errorCount() != 0 ||
                options.get("-Werror") != null && comp.warningCount() != 0)
                return EXIT_ERROR;
        } catch (IOException ex) {
            ioMessage(ex);
            return EXIT_SYSERR;
        } catch (OutOfMemoryError ex) {
            resourceMessage(ex);
            return EXIT_SYSERR;
        } catch (StackOverflowError ex) {
            resourceMessage(ex);
            return EXIT_SYSERR;
        } catch (FatalError ex) {
            feMessage(ex);
            return EXIT_SYSERR;
        } catch (UsageMessageNeededException umne) {
            help();
            return EXIT_CMDERR; // will cause usage message to be printed
        } catch (AnnotationProcessingError ex) {
            apMessage(ex);
            return EXIT_ABNORMAL;
        } catch (sun.misc.ServiceConfigurationError sce) {
            sceMessage(sce);
            return EXIT_ABNORMAL;
        } catch (Throwable ex) {
            bugMessage(ex);
            return EXIT_ABNORMAL;
        } finally {
            if (comp != null) {
                comp.close();
                genSourceFileNames.addAll(comp.getSourceFileNames());
                genClassFileNames.addAll(comp.getClassFileNames());
            }
            sourceFileNames = new java.util.LinkedList<String>();
            classFileNames  = new java.util.LinkedList<String>();
        }
        return exitCode;
    }

    /** Print a message reporting an internal error.
     */
    void bugMessage(Throwable ex) {
        Bark.printLines(out, getLocalizedString("msg.bug",
                                               AptJavaCompiler.version()));
        ex.printStackTrace(out);
    }

    /** Print a message reporting an fatal error.
     */
    void apMessage(AnnotationProcessingError ex) {
        Bark.printLines(out, getLocalizedString("misc.Problem"));
        ex.getCause().printStackTrace(out);
    }

    /** Print a message about sun.misc.Service problem.
     */
    void sceMessage(sun.misc.ServiceConfigurationError ex) {
        Bark.printLines(out, getLocalizedString("misc.SunMiscService"));
        ex.printStackTrace(out);
    }

    /** Print a message reporting an fatal error.
     */
    void feMessage(Throwable ex) {
        Bark.printLines(out, ex.toString());
    }

    /** Print a message reporting an input/output error.
     */
    void ioMessage(Throwable ex) {
        Bark.printLines(out, getLocalizedString("msg.io"));
        ex.printStackTrace(out);
    }

    /** Print a message reporting an out-of-resources error.
     */
    void resourceMessage(Throwable ex) {
        Bark.printLines(out, getLocalizedString("msg.resource"));
        ex.printStackTrace(out);
    }

    /* ************************************************************************
     * Internationalization
     *************************************************************************/

    /** Find a localized string in the resource bundle.
     *  @param key     The key for the localized string.
     */
    private static String getLocalizedString(String key, Object... args) {
        return getText(key, args);
    }

    private static final String javacRB =
        "com.sun.tools.javac.resources.javac";

    private static final String aptRB =
        "com.sun.tools.apt.resources.apt";

    private static ResourceBundle messageRBjavac;
    private static ResourceBundle messageRBapt;

    /** Initialize ResourceBundle.
     */
    private static void initResource() {
        try {
            messageRBapt   = ResourceBundle.getBundle(aptRB);
            messageRBjavac = ResourceBundle.getBundle(javacRB);
        } catch (MissingResourceException e) {
            Error x = new FatalError("Fatal Error: Resource for apt or javac is missing");
            x.initCause(e);
            throw x;
        }
    }

    /** Get and format message string from resource.
     */
    private static String getText(String key, Object... _args) {
        String[] args = new String[_args.length];
        for (int i=0; i<_args.length; i++) {
            args[i] = "" + _args[i];
        }
        if (messageRBapt == null || messageRBjavac == null )
            initResource();
        try {
            return MessageFormat.format(messageRBapt.getString("apt." + key),
                                        (Object[]) args);
        } catch (MissingResourceException e) {
            try {
                return MessageFormat.format(messageRBjavac.getString("javac." + key),
                                            (Object[]) args);
            } catch (MissingResourceException f) {
                String msg = "apt or javac message file broken: key={0} "
                    + "arguments={1}, {2}";
                return MessageFormat.format(msg, (Object[]) args);
            }
        }
    }
}
