Start.java revision 3828:d30434bde0a8
1/*
2 * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.javadoc.internal.tool;
27
28import java.io.File;
29import java.io.IOException;
30import java.io.PrintWriter;
31import java.nio.file.Path;
32import java.text.BreakIterator;
33import java.text.Collator;
34import java.util.ArrayList;
35import java.util.Arrays;
36import java.util.Collection;
37import java.util.Collections;
38import java.util.Comparator;
39import java.util.List;
40import java.util.Locale;
41import java.util.Objects;
42import java.util.Set;
43import java.util.stream.Collectors;
44import java.util.stream.Stream;
45
46import javax.tools.JavaFileManager;
47import javax.tools.JavaFileObject;
48import javax.tools.StandardJavaFileManager;
49import javax.tools.StandardLocation;
50
51import com.sun.tools.javac.api.JavacTrees;
52import com.sun.tools.javac.file.BaseFileManager;
53import com.sun.tools.javac.file.JavacFileManager;
54import com.sun.tools.javac.main.Arguments;
55import com.sun.tools.javac.main.CommandLine;
56import com.sun.tools.javac.main.OptionHelper;
57import com.sun.tools.javac.main.OptionHelper.GrumpyHelper;
58import com.sun.tools.javac.platform.PlatformDescription;
59import com.sun.tools.javac.platform.PlatformUtils;
60import com.sun.tools.javac.util.ClientCodeException;
61import com.sun.tools.javac.util.Context;
62import com.sun.tools.javac.util.Log;
63import com.sun.tools.javac.util.Log.WriterKind;
64import com.sun.tools.javac.util.Options;
65import com.sun.tools.javac.util.StringUtils;
66
67import jdk.javadoc.doclet.Doclet;
68import jdk.javadoc.doclet.Doclet.Option;
69import jdk.javadoc.doclet.DocletEnvironment;
70import jdk.javadoc.internal.tool.Main.Result;
71
72import static javax.tools.DocumentationTool.Location.*;
73
74import static com.sun.tools.javac.main.Option.*;
75import static jdk.javadoc.internal.tool.Main.Result.*;
76
77/**
78 * Main program of Javadoc.
79 * Previously named "Main".
80 *
81 *  <p><b>This is NOT part of any supported API.
82 *  If you write code that depends on this, you do so at your own risk.
83 *  This code and its internal interfaces are subject to change or
84 *  deletion without notice.</b>
85 *
86 * @author Robert Field
87 * @author Neal Gafter (rewrite)
88 */
89public class Start extends ToolOption.Helper {
90
91    @SuppressWarnings("deprecation")
92    private static final Class<?> OldStdDoclet =
93            com.sun.tools.doclets.standard.Standard.class;
94
95    private static final Class<?> StdDoclet =
96            jdk.javadoc.doclets.StandardDoclet.class;
97    /** Context for this invocation. */
98    private final Context context;
99
100    private static final String ProgramName = "javadoc";
101
102    private Messager messager;
103
104    private final String docletName;
105
106    private final ClassLoader classLoader;
107
108    private Class<?> docletClass;
109
110    private Doclet doclet;
111
112    // used to determine the locale for the messager
113    private Locale locale;
114
115
116    /**
117     * In API mode, exceptions thrown while calling the doclet are
118     * propagated using ClientCodeException.
119     */
120    private boolean apiMode;
121
122    private JavaFileManager fileManager;
123
124    Start() {
125        this(null, null, null, null, null, null);
126    }
127
128    Start(PrintWriter outWriter, PrintWriter errWriter) {
129        this(null, null, outWriter, errWriter, null, null);
130    }
131
132    Start(Context context, String programName,
133            PrintWriter outWriter, PrintWriter errWriter,
134            String docletName, ClassLoader classLoader) {
135        this.context = context == null ? new Context() : context;
136        String pname = programName == null ? ProgramName : programName;
137        this.messager = (outWriter == null && errWriter == null)
138                ? new Messager(this.context, pname)
139                : new Messager(this.context, pname, outWriter, errWriter);
140        this.docletName = docletName;
141        this.classLoader = classLoader;
142        this.docletClass = null;
143        this.locale = Locale.getDefault();
144    }
145
146    public Start(Context context) {
147        this.docletClass = null;
148        this.context = Objects.requireNonNull(context);
149        this.apiMode = true;
150        this.docletName = null;
151        this.classLoader = null;
152        this.locale = Locale.getDefault();
153    }
154
155    void initMessager() {
156        if (!apiMode)
157            return;
158        if (messager == null) {
159            Log log = context.get(Log.logKey);
160            if (log instanceof Messager) {
161                messager = (Messager) log;
162            } else {
163                PrintWriter out = context.get(Log.errKey);
164                messager = (out == null)
165                        ? new Messager(context, ProgramName)
166                        : new Messager(context, ProgramName, out, out);
167            }
168        }
169    }
170
171    /**
172     * Usage
173     */
174    @Override
175    void usage() {
176        usage("main.usage", OptionKind.STANDARD, "main.usage.foot");
177    }
178
179    @Override
180    void Xusage() {
181        usage("main.Xusage", OptionKind.EXTENDED, "main.Xusage.foot");
182    }
183
184    private void usage(String headerKey, OptionKind kind, String footerKey) {
185        messager.notice(headerKey);
186        showToolOptions(kind);
187
188        // let doclet print usage information
189        if (docletClass != null) {
190            String name = doclet.getName();
191            messager.notice("main.doclet.usage.header", name);
192            showDocletOptions(kind == OptionKind.EXTENDED
193                    ? Option.Kind.EXTENDED
194                    : Option.Kind.STANDARD);
195        }
196        if (footerKey != null)
197            messager.notice(footerKey);
198    }
199
200    void showToolOptions(OptionKind kind) {
201        Comparator<ToolOption> comp = new Comparator<ToolOption>() {
202            final Collator collator = Collator.getInstance(Locale.US);
203            { collator.setStrength(Collator.PRIMARY); }
204
205            @Override
206            public int compare(ToolOption o1, ToolOption o2) {
207                return collator.compare(o1.primaryName, o2.primaryName);
208            }
209        };
210
211        Stream.of(ToolOption.values())
212                    .filter(opt -> opt.kind == kind)
213                    .sorted(comp)
214                    .forEach(this::showToolOption);
215    }
216
217    void showToolOption(ToolOption option) {
218        List<String> names = option.getNames();
219        String parameters;
220        if (option.hasArg || option.primaryName.endsWith(":")) {
221            String sep = (option == ToolOption.J) || option.primaryName.endsWith(":") ? "" : " ";
222            parameters = sep + option.getParameters(messager);
223        } else {
224            parameters = "";
225        }
226        String description = option.getDescription(messager);
227        showUsage(names, parameters, description);
228    }
229
230    void showDocletOptions(Option.Kind kind) {
231        Comparator<Doclet.Option> comp = new Comparator<Doclet.Option>() {
232            final Collator collator = Collator.getInstance(Locale.US);
233            { collator.setStrength(Collator.PRIMARY); }
234
235            @Override
236            public int compare(Doclet.Option o1, Doclet.Option o2) {
237                return collator.compare(o1.getNames().get(0), o2.getNames().get(0));
238            }
239        };
240
241        doclet.getSupportedOptions().stream()
242                .filter(opt -> opt.getKind() == kind)
243                .sorted(comp)
244                .forEach(this::showDocletOption);
245    }
246
247    void showDocletOption(Doclet.Option option) {
248        List<String> names = option.getNames();
249        String parameters;
250        String optname = names.get(0);
251        if (option.getArgumentCount() > 0 || optname.endsWith(":")) {
252            String sep = optname.endsWith(":") ? "" : " ";
253            parameters = sep + option.getParameters();
254        } else {
255            parameters = "";
256        }
257        String description = option.getDescription();
258        showUsage(names, parameters, description);
259    }
260
261    // The following constants are intended to format the output to
262    // be similar to that of the java launcher: i.e. "java -help".
263
264    /** The indent for the option synopsis. */
265    private static final String SMALL_INDENT = "    ";
266    /** The automatic indent for the description. */
267    private static final String LARGE_INDENT = "                  ";
268    /** The space allowed for the synopsis, if the description is to be shown on the same line. */
269    private static final int DEFAULT_SYNOPSIS_WIDTH = 13;
270    /** The nominal maximum line length, when seeing if text will fit on a line. */
271    private static final int DEFAULT_MAX_LINE_LENGTH = 80;
272    /** The format for a single-line help entry. */
273    private static final String COMPACT_FORMAT = SMALL_INDENT + "%-" + DEFAULT_SYNOPSIS_WIDTH + "s %s";
274
275    void showUsage(List<String> names, String parameters, String description) {
276        String synopses = names.stream()
277                .map(s -> s + parameters)
278                .collect(Collectors.joining(", "));
279        // If option synopses and description fit on a single line of reasonable length,
280        // display using COMPACT_FORMAT
281        if (synopses.length() < DEFAULT_SYNOPSIS_WIDTH
282                && !description.contains("\n")
283                && (SMALL_INDENT.length() + DEFAULT_SYNOPSIS_WIDTH + 1 + description.length() <= DEFAULT_MAX_LINE_LENGTH)) {
284            messager.printNotice(String.format(COMPACT_FORMAT, synopses, description));
285            return;
286        }
287
288        // If option synopses fit on a single line of reasonable length, show that;
289        // otherwise, show 1 per line
290        if (synopses.length() <= DEFAULT_MAX_LINE_LENGTH) {
291            messager.printNotice(SMALL_INDENT + synopses);
292        } else {
293            for (String name: names) {
294                messager.printNotice(SMALL_INDENT + name + parameters);
295            }
296        }
297
298        // Finally, show the description
299        messager.printNotice(LARGE_INDENT + description.replace("\n", "\n" + LARGE_INDENT));
300    }
301
302
303    /**
304     * Main program - external wrapper. In order to maintain backward
305     * CLI  compatibility, we dispatch to the old tool or the old doclet's
306     * Start mechanism, based on the options present on the command line
307     * with the following precedence:
308     *   1. presence of -Xold, dispatch to old tool
309     *   2. doclet variant, if old, dispatch to old Start
310     *   3. taglet variant, if old, dispatch to old Start
311     *
312     * Thus the presence of -Xold switches the tool, soon after command files
313     * if any, are expanded, this is performed here, noting that the messager
314     * is available at this point in time.
315     * The doclet/taglet tests are performed in the begin method, further on,
316     * this is to minimize argument processing and most importantly the impact
317     * of class loader creation, needed to detect the doclet/taglet class variants.
318     */
319    @SuppressWarnings("deprecation")
320    Result begin(String... argv) {
321        // Preprocess @file arguments
322        try {
323            argv = CommandLine.parse(argv);
324        } catch (IOException e) {
325            error("main.cant.read", e.getMessage());
326            return ERROR;
327        }
328
329        if (argv.length > 0 && "-Xold".equals(argv[0])) {
330            warn("main.legacy_api");
331            String[] nargv = Arrays.copyOfRange(argv, 1, argv.length);
332            int rc = com.sun.tools.javadoc.Main.execute(
333                    messager.programName,
334                    messager.getWriter(WriterKind.ERROR),
335                    messager.getWriter(WriterKind.WARNING),
336                    messager.getWriter(WriterKind.NOTICE),
337                    "com.sun.tools.doclets.standard.Standard",
338                    nargv);
339            return (rc == 0) ? OK : ERROR;
340        }
341        return begin(Arrays.asList(argv), Collections.emptySet());
342    }
343
344    // Called by 199 API.
345    public boolean begin(Class<?> docletClass,
346            Iterable<String> options,
347            Iterable<? extends JavaFileObject> fileObjects) {
348        this.docletClass = docletClass;
349        List<String> opts = new ArrayList<>();
350        for (String opt: options)
351            opts.add(opt);
352
353        return begin(opts, fileObjects).isOK();
354    }
355
356    @SuppressWarnings("deprecation")
357    private Result begin(List<String> options, Iterable<? extends JavaFileObject> fileObjects) {
358        fileManager = context.get(JavaFileManager.class);
359        if (fileManager == null) {
360            JavacFileManager.preRegister(context);
361            fileManager = context.get(JavaFileManager.class);
362            if (fileManager instanceof BaseFileManager) {
363                ((BaseFileManager) fileManager).autoClose = true;
364            }
365        }
366
367        // locale, doclet and maybe taglet, needs to be determined first
368        try {
369            docletClass = preprocess(fileManager, options);
370        } catch (ToolException te) {
371            if (!te.result.isOK()) {
372                if (te.message != null) {
373                    messager.printError(te.message);
374                }
375                Throwable t = te.getCause();
376                dumpStack(t == null ? te : t);
377            }
378            return te.result;
379        } catch (OptionException oe) {
380            if (oe.message != null) {
381                messager.printError(oe.message);
382            }
383            oe.m.run();
384            Throwable t = oe.getCause();
385            dumpStack(t == null ? oe : t);
386            return oe.result;
387        }
388        if (jdk.javadoc.doclet.Doclet.class.isAssignableFrom(docletClass)) {
389            // no need to dispatch to old, safe to init now
390            initMessager();
391            messager.setLocale(locale);
392            try {
393                Object o = docletClass.getConstructor().newInstance();
394                doclet = (Doclet) o;
395            } catch (ReflectiveOperationException exc) {
396                if (apiMode) {
397                    throw new ClientCodeException(exc);
398                }
399                error("main.could_not_instantiate_class", docletClass);
400                return ERROR;
401            }
402        } else {
403            if (apiMode) {
404                com.sun.tools.javadoc.main.Start ostart
405                        = new com.sun.tools.javadoc.main.Start(context);
406                return ostart.begin(docletClass, options, fileObjects)
407                        ? OK
408                        : ERROR;
409            }
410            warn("main.legacy_api");
411            String[] array = options.toArray(new String[options.size()]);
412            int rc = com.sun.tools.javadoc.Main.execute(
413                    messager.programName,
414                    messager.getWriter(WriterKind.ERROR),
415                    messager.getWriter(WriterKind.WARNING),
416                    messager.getWriter(WriterKind.NOTICE),
417                    "com.sun.tools.doclets.standard.Standard",
418                    array);
419            return (rc == 0) ? OK : ERROR;
420        }
421
422        Result result = OK;
423        try {
424            result = parseAndExecute(options, fileObjects)
425                    ? OK
426                    : ERROR;
427        } catch (com.sun.tools.javac.main.Option.InvalidValueException e) {
428            messager.printError(e.getMessage());
429            Throwable t = e.getCause();
430            dumpStack(t == null ? e : t);
431            return ERROR;
432        } catch (OptionException toe) {
433            if (toe.message != null)
434                messager.printError(toe.message);
435
436            toe.m.run();
437            Throwable t = toe.getCause();
438            dumpStack(t == null ? toe : t);
439            return toe.result;
440        } catch (ToolException exc) {
441            if (exc.message != null) {
442                messager.printError(exc.message);
443            }
444            Throwable t = exc.getCause();
445            if (result == ABNORMAL) {
446                reportInternalError(t == null ? exc : t);
447            } else {
448                dumpStack(t == null ? exc : t);
449            }
450            return exc.result;
451        } catch (OutOfMemoryError ee) {
452            error("main.out.of.memory");
453            result = SYSERR;
454            dumpStack(ee);
455        } catch (ClientCodeException e) {
456            // simply rethrow these exceptions, to be caught and handled by JavadocTaskImpl
457            throw e;
458        } catch (Error | Exception ee) {
459            error("main.fatal.error", ee);
460            reportInternalError(ee);
461            result = ABNORMAL;
462        } finally {
463            if (fileManager != null
464                    && fileManager instanceof BaseFileManager
465                    && ((BaseFileManager) fileManager).autoClose) {
466                try {
467                    fileManager.close();
468                } catch (IOException ignore) {}
469            }
470            boolean haveErrorWarnings = messager.hasErrors()
471                    || (rejectWarnings && messager.hasWarnings());
472            if (!result.isOK() && !haveErrorWarnings) {
473                // the doclet failed, but nothing reported, flag it!.
474                error("main.unknown.error");
475            }
476            if (haveErrorWarnings && result.isOK()) {
477                result = ERROR;
478            }
479            messager.printErrorWarningCounts();
480            messager.flush();
481        }
482        return result;
483    }
484
485    private void reportInternalError(Throwable t) {
486        messager.printErrorUsingKey("doclet.internal.report.bug");
487        dumpStack(true, t);
488    }
489
490    private void dumpStack(Throwable t) {
491        dumpStack(false, t);
492    }
493
494    private void dumpStack(boolean enabled, Throwable t) {
495        if (t != null && (enabled || dumpOnError)) {
496            t.printStackTrace(System.err);
497        }
498    }
499
500    /**
501     * Main program - internal
502     */
503    @SuppressWarnings("unchecked")
504    private boolean parseAndExecute(List<String> argList, Iterable<? extends JavaFileObject> fileObjects)
505            throws ToolException, OptionException, com.sun.tools.javac.main.Option.InvalidValueException {
506        long tm = System.currentTimeMillis();
507
508        List<String> javaNames = new ArrayList<>();
509
510        compOpts = Options.instance(context);
511
512        // Make sure no obsolete source/target messages are reported
513        try {
514            com.sun.tools.javac.main.Option.XLINT_CUSTOM.process(getOptionHelper(), "-Xlint:-options");
515        } catch (com.sun.tools.javac.main.Option.InvalidValueException ignore) {
516        }
517
518        doclet.init(locale, messager);
519        parseArgs(argList, javaNames);
520
521        Arguments arguments = Arguments.instance(context);
522        arguments.init(ProgramName);
523        arguments.allowEmpty();
524        arguments.validate();
525
526        if (fileManager instanceof BaseFileManager) {
527            ((BaseFileManager) fileManager).handleOptions(fileManagerOpts);
528        }
529
530        String platformString = compOpts.get("--release");
531
532        if (platformString != null) {
533            if (compOpts.isSet("-source")) {
534                String text = messager.getText("main.release.bootclasspath.conflict", "-source");
535                throw new ToolException(CMDERR, text);
536            }
537            if (fileManagerOpts.containsKey(BOOT_CLASS_PATH)) {
538                String text = messager.getText("main.release.bootclasspath.conflict",
539                        BOOT_CLASS_PATH.getPrimaryName());
540                throw new ToolException(CMDERR, text);
541            }
542
543            PlatformDescription platformDescription =
544                    PlatformUtils.lookupPlatformDescription(platformString);
545
546            if (platformDescription == null) {
547                String text = messager.getText("main.unsupported.release.version", platformString);
548                throw new IllegalArgumentException(text);
549            }
550
551            compOpts.put(SOURCE, platformDescription.getSourceVersion());
552
553            context.put(PlatformDescription.class, platformDescription);
554
555            Collection<Path> platformCP = platformDescription.getPlatformPath();
556
557            if (platformCP != null) {
558                if (fileManager instanceof StandardJavaFileManager) {
559                    StandardJavaFileManager sfm = (StandardJavaFileManager) fileManager;
560                    try {
561                        sfm.setLocationFromPaths(StandardLocation.PLATFORM_CLASS_PATH, platformCP);
562                    } catch (IOException ioe) {
563                        throw new ToolException(SYSERR, ioe.getMessage(), ioe);
564                    }
565                } else {
566                    String text = messager.getText("main.release.not.standard.file.manager",
567                                                    platformString);
568                    throw new ToolException(ABNORMAL, text);
569                }
570            }
571        }
572
573        compOpts.notifyListeners();
574        List<String> modules = (List<String>) jdtoolOpts.computeIfAbsent(ToolOption.MODULE,
575                s -> Collections.EMPTY_LIST);
576
577        if (modules.isEmpty()) {
578            List<String> subpkgs = (List<String>) jdtoolOpts.computeIfAbsent(ToolOption.SUBPACKAGES,
579                    s -> Collections.EMPTY_LIST);
580            if (subpkgs.isEmpty()) {
581                if (javaNames.isEmpty() && isEmpty(fileObjects)) {
582                    String text = messager.getText("main.No_modules_packages_or_classes_specified");
583                    throw new ToolException(CMDERR, text);
584                }
585            }
586        }
587
588        JavadocTool comp = JavadocTool.make0(context);
589        if (comp == null) return false;
590
591        DocletEnvironment docEnv = comp.getEnvironment(jdtoolOpts,
592                javaNames,
593                fileObjects);
594
595        // release resources
596        comp = null;
597
598        if (breakiterator || !locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
599            JavacTrees trees = JavacTrees.instance(context);
600            trees.setBreakIterator(BreakIterator.getSentenceInstance(locale));
601        }
602        // pass off control to the doclet
603        boolean ok = docEnv != null;
604        if (ok) ok = doclet.run(docEnv);
605
606        // We're done.
607        if (compOpts.get("-verbose") != null) {
608            tm = System.currentTimeMillis() - tm;
609            messager.notice("main.done_in", Long.toString(tm));
610        }
611
612        return ok;
613    }
614
615    boolean matches(List<String> names, String arg) {
616        for (String name : names) {
617            if (StringUtils.toLowerCase(name).equals(StringUtils.toLowerCase(arg)))
618                return true;
619        }
620        return false;
621    }
622
623    boolean matches(Doclet.Option option, String arg) {
624        if (matches(option.getNames(), arg))
625             return true;
626        int sep = arg.indexOf(':');
627        String targ = arg.substring(0, sep + 1);
628        return matches(option.getNames(), targ);
629    }
630
631    Set<? extends Doclet.Option> docletOptions = null;
632    int handleDocletOptions(int idx, List<String> args, boolean isToolOption)
633            throws OptionException {
634        if (docletOptions == null) {
635            docletOptions = doclet.getSupportedOptions();
636        }
637        String arg = args.get(idx);
638        String argBase, argVal;
639        if (arg.startsWith("--") && arg.contains("=")) {
640            int sep = arg.indexOf("=");
641            argBase = arg.substring(0, sep);
642            argVal = arg.substring(sep + 1);
643        } else {
644            argBase = arg;
645            argVal = null;
646        }
647        String text = null;
648        for (Doclet.Option opt : docletOptions) {
649            if (matches(opt, argBase)) {
650                if (argVal != null) {
651                    switch (opt.getArgumentCount()) {
652                        case 0:
653                            text = messager.getText("main.unnecessary_arg_provided", argBase);
654                            throw new OptionException(ERROR, this::usage, text);
655                        case 1:
656                            opt.process(arg, Arrays.asList(argVal));
657                            break;
658                        default:
659                            text = messager.getText("main.only_one_argument_with_equals", argBase);
660                            throw new OptionException(ERROR, this::usage, text);
661                    }
662                } else {
663                    if (args.size() - idx -1 < opt.getArgumentCount()) {
664                        text = messager.getText("main.requires_argument", arg);
665                        throw new OptionException(ERROR, this::usage, text);
666                    }
667                    opt.process(arg, args.subList(idx + 1, args.size()));
668                    idx += opt.getArgumentCount();
669                }
670                return idx;
671            }
672        }
673        // check if arg is accepted by the tool before emitting error
674        if (!isToolOption) {
675            text = messager.getText("main.invalid_flag", arg);
676            throw new OptionException(ERROR, this::usage, text);
677        }
678        return idx;
679    }
680
681    private Class<?> preprocess(JavaFileManager jfm,
682            List<String> argv) throws ToolException, OptionException {
683        // doclet specifying arguments
684        String userDocletPath = null;
685        String userDocletName = null;
686
687        // taglet specifying arguments, since tagletpath is a doclet
688        // functionality, assume they are repeated and inspect all.
689        List<File> userTagletPath = new ArrayList<>();
690        List<String> userTagletNames = new ArrayList<>();
691
692        // Step 1: loop through the args, set locale early on, if found.
693        for (int i = 0 ; i < argv.size() ; i++) {
694            String arg = argv.get(i);
695            if (arg.equals(ToolOption.DUMPONERROR.primaryName)) {
696                dumpOnError = true;
697            } else if (arg.equals(ToolOption.LOCALE.primaryName)) {
698                checkOneArg(argv, i++);
699                String lname = argv.get(i);
700                locale = getLocale(lname);
701            } else if (arg.equals(ToolOption.DOCLET.primaryName)) {
702                checkOneArg(argv, i++);
703                if (userDocletName != null) {
704                    if (apiMode) {
705                        throw new IllegalArgumentException("More than one doclet specified (" +
706                                userDocletName + " and " + argv.get(i) + ").");
707                    }
708                    String text = messager.getText("main.more_than_one_doclet_specified_0_and_1",
709                            userDocletName, argv.get(i));
710                    throw new ToolException(CMDERR, text);
711                }
712                if (docletName != null) {
713                    if (apiMode) {
714                        throw new IllegalArgumentException("More than one doclet specified (" +
715                                docletName + " and " + argv.get(i) + ").");
716                    }
717                    String text = messager.getText("main.more_than_one_doclet_specified_0_and_1",
718                            docletName, argv.get(i));
719                    throw new ToolException(CMDERR, text);
720                }
721                userDocletName = argv.get(i);
722            } else if (arg.equals(ToolOption.DOCLETPATH.primaryName)) {
723                checkOneArg(argv, i++);
724                if (userDocletPath == null) {
725                    userDocletPath = argv.get(i);
726                } else {
727                    userDocletPath += File.pathSeparator + argv.get(i);
728                }
729            } else if ("-taglet".equals(arg)) {
730                userTagletNames.add(argv.get(i + 1));
731            } else if ("-tagletpath".equals(arg)) {
732                for (String pathname : argv.get(i + 1).split(File.pathSeparator)) {
733                    userTagletPath.add(new File(pathname));
734                }
735            }
736        }
737
738        // Step 2: a doclet is provided, nothing more to do.
739        if (docletClass != null) {
740            return docletClass;
741        }
742
743        // Step 3: doclet name specified ? if so find a ClassLoader,
744        // and load it.
745        if (userDocletName != null) {
746            ClassLoader cl = classLoader;
747            if (cl == null) {
748                if (!fileManager.hasLocation(DOCLET_PATH)) {
749                    List<File> paths = new ArrayList<>();
750                    if (userDocletPath != null) {
751                        for (String pathname : userDocletPath.split(File.pathSeparator)) {
752                            paths.add(new File(pathname));
753                        }
754                    }
755                    try {
756                        ((StandardJavaFileManager)fileManager).setLocation(DOCLET_PATH, paths);
757                    } catch (IOException ioe) {
758                        if (apiMode) {
759                            throw new IllegalArgumentException("Could not set location for " +
760                                    userDocletPath, ioe);
761                        }
762                        String text = messager.getText("main.doclet_could_not_set_location",
763                                userDocletPath);
764                        throw new ToolException(CMDERR, text, ioe);
765                    }
766                }
767                cl = fileManager.getClassLoader(DOCLET_PATH);
768                if (cl == null) {
769                    // despite doclet specified on cmdline no classloader found!
770                    if (apiMode) {
771                        throw new IllegalArgumentException("Could not obtain classloader to load "
772                                + userDocletPath);
773                    }
774                    String text = messager.getText("main.doclet_no_classloader_found",
775                            userDocletName);
776                    throw new ToolException(CMDERR, text);
777                }
778            }
779            try {
780                Class<?> klass = cl.loadClass(userDocletName);
781                return klass;
782            } catch (ClassNotFoundException cnfe) {
783                if (apiMode) {
784                    throw new IllegalArgumentException("Cannot find doclet class " + userDocletName,
785                            cnfe);
786                }
787                String text = messager.getText("main.doclet_class_not_found", userDocletName);
788                throw new ToolException(CMDERR, text, cnfe);
789            }
790        }
791
792        // Step 4: we have a doclet, try loading it
793        if (docletName != null) {
794            try {
795                return Class.forName(docletName, true, getClass().getClassLoader());
796            } catch (ClassNotFoundException cnfe) {
797                if (apiMode) {
798                    throw new IllegalArgumentException("Cannot find doclet class " + userDocletName);
799                }
800                String text = messager.getText("main.doclet_class_not_found", userDocletName);
801                throw new ToolException(CMDERR, text, cnfe);
802            }
803        }
804
805        // Step 5: we don't have a doclet specified, do we have taglets ?
806        if (!userTagletNames.isEmpty() && hasOldTaglet(userTagletNames, userTagletPath)) {
807            // found a bogey, return the old doclet
808            return OldStdDoclet;
809        }
810
811        // finally
812        return StdDoclet;
813    }
814
815    /*
816     * This method returns true iff it finds a legacy taglet, but for
817     * all other conditions including errors it returns false, allowing
818     * nature to take its own course.
819     */
820    @SuppressWarnings("deprecation")
821    private boolean hasOldTaglet(List<String> tagletNames, List<File> tagletPaths) throws ToolException {
822        if (!fileManager.hasLocation(TAGLET_PATH)) {
823            try {
824                ((StandardJavaFileManager) fileManager).setLocation(TAGLET_PATH, tagletPaths);
825            } catch (IOException ioe) {
826                String text = messager.getText("main.doclet_could_not_set_location", tagletPaths);
827                throw new ToolException(CMDERR, text, ioe);
828            }
829        }
830        ClassLoader cl = fileManager.getClassLoader(TAGLET_PATH);
831        if (cl == null) {
832            // no classloader found!
833            String text = messager.getText("main.doclet_no_classloader_found", tagletNames.get(0));
834            throw new ToolException(CMDERR, text);
835        }
836        for (String tagletName : tagletNames) {
837            try {
838                Class<?> klass = cl.loadClass(tagletName);
839                if (com.sun.tools.doclets.Taglet.class.isAssignableFrom(klass)) {
840                    return true;
841                }
842            } catch (ClassNotFoundException cnfe) {
843                String text = messager.getText("main.doclet_class_not_found", tagletName);
844                throw new ToolException(CMDERR, text, cnfe);
845            }
846        }
847        return false;
848    }
849
850    private void parseArgs(List<String> args, List<String> javaNames) throws ToolException,
851            OptionException, com.sun.tools.javac.main.Option.InvalidValueException {
852        for (int i = 0 ; i < args.size() ; i++) {
853            String arg = args.get(i);
854            ToolOption o = ToolOption.get(arg);
855            if (o != null) {
856                // handle a doclet argument that may be needed however
857                // don't increment the index, and allow the tool to consume args
858                handleDocletOptions(i, args, true);
859                if (o.hasArg) {
860                    if (arg.startsWith("--") && arg.contains("=")) {
861                        o.process(this, arg.substring(arg.indexOf('=') + 1));
862                    } else {
863                        checkOneArg(args, i++);
864                        o.process(this, args.get(i));
865                    }
866                } else if (o.hasSuffix) {
867                    o.process(this, arg);
868                } else {
869                    o.process(this);
870                }
871            } else if (arg.startsWith("-XD")) {
872                // hidden javac options
873                String s = arg.substring("-XD".length());
874                int eq = s.indexOf('=');
875                String key = (eq < 0) ? s : s.substring(0, eq);
876                String value = (eq < 0) ? s : s.substring(eq+1);
877                compOpts.put(key, value);
878            } else if (arg.startsWith("-")) {
879                i = handleDocletOptions(i, args, false);
880            } else {
881                javaNames.add(arg);
882            }
883        }
884    }
885
886    private <T> boolean isEmpty(Iterable<T> iter) {
887        return !iter.iterator().hasNext();
888    }
889
890    /**
891     * Check the one arg option.
892     * Error and exit if one argument is not provided.
893     */
894    private void checkOneArg(List<String> args, int index) throws OptionException {
895        if ((index + 1) >= args.size() || args.get(index + 1).startsWith("-d")) {
896            String text = messager.getText("main.requires_argument", args.get(index));
897            throw new OptionException(CMDERR, this::usage, text);
898        }
899    }
900
901    void error(String key, Object... args) {
902        messager.printErrorUsingKey(key, args);
903    }
904
905    void warn(String key, Object... args)  {
906        messager.printWarningUsingKey(key, args);
907    }
908
909    /**
910     * Get the locale if specified on the command line
911     * else return null and if locale option is not used
912     * then return default locale.
913     */
914    private Locale getLocale(String localeName) throws ToolException {
915        Locale userlocale = null;
916        if (localeName == null || localeName.isEmpty()) {
917            return Locale.getDefault();
918        }
919        int firstuscore = localeName.indexOf('_');
920        int seconduscore = -1;
921        String language = null;
922        String country = null;
923        String variant = null;
924        if (firstuscore == 2) {
925            language = localeName.substring(0, firstuscore);
926            seconduscore = localeName.indexOf('_', firstuscore + 1);
927            if (seconduscore > 0) {
928                if (seconduscore != firstuscore + 3
929                        || localeName.length() <= seconduscore + 1) {
930                    String text = messager.getText("main.malformed_locale_name", localeName);
931                    throw new ToolException(CMDERR, text);
932                }
933                country = localeName.substring(firstuscore + 1,
934                        seconduscore);
935                variant = localeName.substring(seconduscore + 1);
936            } else if (localeName.length() == firstuscore + 3) {
937                country = localeName.substring(firstuscore + 1);
938            } else {
939                String text = messager.getText("main.malformed_locale_name", localeName);
940                throw new ToolException(CMDERR, text);
941            }
942        } else if (firstuscore == -1 && localeName.length() == 2) {
943            language = localeName;
944        } else {
945            String text = messager.getText("main.malformed_locale_name", localeName);
946            throw new ToolException(CMDERR, text);
947        }
948        userlocale = searchLocale(language, country, variant);
949        if (userlocale == null) {
950            String text = messager.getText("main.illegal_locale_name", localeName);
951            throw new ToolException(CMDERR, text);
952        } else {
953            return userlocale;
954        }
955    }
956
957    /**
958     * Search the locale for specified language, specified country and
959     * specified variant.
960     */
961    private Locale searchLocale(String language, String country,
962                                String variant) {
963        for (Locale loc : Locale.getAvailableLocales()) {
964            if (loc.getLanguage().equals(language) &&
965                (country == null || loc.getCountry().equals(country)) &&
966                (variant == null || loc.getVariant().equals(variant))) {
967                return loc;
968            }
969        }
970        return null;
971    }
972
973    @Override
974    OptionHelper getOptionHelper() {
975        return new GrumpyHelper(messager) {
976            @Override
977            public String get(com.sun.tools.javac.main.Option option) {
978                return compOpts.get(option);
979            }
980
981            @Override
982            public void put(String name, String value) {
983                compOpts.put(name, value);
984            }
985
986            @Override
987            public void remove(String name) {
988                compOpts.remove(name);
989            }
990
991            @Override
992            public boolean handleFileManagerOption(com.sun.tools.javac.main.Option option, String value) {
993                fileManagerOpts.put(option, value);
994                return true;
995            }
996        };
997    }
998
999    @Override
1000    String getLocalizedMessage(String msg, Object... args) {
1001        return messager.getText(msg, args);
1002    }
1003}
1004