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