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 
  26 package jdk.javadoc.internal.tool;
  27 
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.io.PrintWriter;
  31 import java.nio.file.Path;
  32 import java.text.BreakIterator;
  33 import java.text.Collator;
  34 import java.util.ArrayList;
  35 import java.util.Arrays;
  36 import java.util.Collection;
  37 import java.util.Collections;
  38 import java.util.Comparator;
  39 import java.util.List;
  40 import java.util.Locale;
  41 import java.util.Objects;
  42 import java.util.Set;
  43 import java.util.stream.Collectors;
  44 import java.util.stream.Stream;
  45 
  46 import javax.tools.JavaFileManager;
  47 import javax.tools.JavaFileObject;
  48 import javax.tools.StandardJavaFileManager;
  49 import javax.tools.StandardLocation;
  50 
  51 import com.sun.tools.javac.api.JavacTrees;
  52 import com.sun.tools.javac.file.BaseFileManager;
  53 import com.sun.tools.javac.file.JavacFileManager;
  54 import com.sun.tools.javac.main.Arguments;
  55 import com.sun.tools.javac.main.CommandLine;
  56 import com.sun.tools.javac.main.OptionHelper;
  57 import com.sun.tools.javac.main.OptionHelper.GrumpyHelper;
  58 import com.sun.tools.javac.platform.PlatformDescription;
  59 import com.sun.tools.javac.platform.PlatformUtils;
  60 import com.sun.tools.javac.util.ClientCodeException;
  61 import com.sun.tools.javac.util.Context;
  62 import com.sun.tools.javac.util.Log;
  63 import com.sun.tools.javac.util.Log.WriterKind;
  64 import com.sun.tools.javac.util.Options;
  65 import com.sun.tools.javac.util.StringUtils;
  66 
  67 import jdk.javadoc.doclet.Doclet;
  68 import jdk.javadoc.doclet.Doclet.Option;
  69 import jdk.javadoc.doclet.DocletEnvironment;
  70 import jdk.javadoc.internal.tool.Main.Result;
  71 
  72 import static javax.tools.DocumentationTool.Location.*;
  73 
  74 import static com.sun.tools.javac.main.Option.*;
  75 import 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  */
  89 public 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.doclet.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         } catch (com.sun.tools.javac.main.Option.InvalidValueException e) {
 426             messager.printError(e.getMessage());
 427             Throwable t = e.getCause();
 428             dumpStack(t == null ? e : t);
 429             return ERROR;
 430         } catch (OptionException toe) {
 431             if (toe.message != null)
 432                 messager.printError(toe.message);
 433 
 434             toe.m.run();
 435             Throwable t = toe.getCause();
 436             dumpStack(t == null ? toe : t);
 437             return toe.result;
 438         } catch (ToolException exc) {
 439             if (exc.message != null) {
 440                 messager.printError(exc.message);
 441             }
 442             Throwable t = exc.getCause();
 443             if (result == ABNORMAL) {
 444                 reportInternalError(t == null ? exc : t);
 445             } else {
 446                 dumpStack(t == null ? exc : t);
 447             }
 448             return exc.result;
 449         } catch (OutOfMemoryError ee) {
 450             error("main.out.of.memory");
 451             result = SYSERR;
 452             dumpStack(ee);
 453         } catch (ClientCodeException e) {
 454             // simply rethrow these exceptions, to be caught and handled by JavadocTaskImpl
 455             throw e;
 456         } catch (Error | Exception ee) {
 457             error("main.fatal.error", ee);
 458             reportInternalError(ee);
 459             result = ABNORMAL;
 460         } finally {
 461             if (fileManager != null
 462                     && fileManager instanceof BaseFileManager
 463                     && ((BaseFileManager) fileManager).autoClose) {
 464                 try {
 465                     fileManager.close();
 466                 } catch (IOException ignore) {}
 467             }
 468             boolean haveErrorWarnings = messager.hasErrors()
 469                     || (rejectWarnings && messager.hasWarnings());
 470             if (!result.isOK() && !haveErrorWarnings) {
 471                 // the doclet failed, but nothing reported, flag it!.
 472                 error("main.unknown.error");
 473             }
 474             if (haveErrorWarnings && result.isOK()) {
 475                 result = ERROR;
 476             }
 477             messager.printErrorWarningCounts();
 478             messager.flush();
 479         }
 480         return result;
 481     }
 482 
 483     private void reportInternalError(Throwable t) {
 484         messager.printErrorUsingKey("doclet.internal.report.bug");
 485         dumpStack(true, t);
 486     }
 487 
 488     private void dumpStack(Throwable t) {
 489         dumpStack(false, t);
 490     }
 491 
 492     private void dumpStack(boolean enabled, Throwable t) {
 493         if (t != null && (enabled || dumpOnError)) {
 494             t.printStackTrace(System.err);
 495         }
 496     }
 497 
 498     /**
 499      * Main program - internal
 500      */
 501     @SuppressWarnings("unchecked")
 502     private Result parseAndExecute(List<String> argList, Iterable<? extends JavaFileObject> fileObjects)
 503             throws ToolException, OptionException, com.sun.tools.javac.main.Option.InvalidValueException {
 504         long tm = System.currentTimeMillis();
 505 
 506         List<String> javaNames = new ArrayList<>();
 507 
 508         compOpts = Options.instance(context);
 509 
 510         // Make sure no obsolete source/target messages are reported
 511         try {
 512             com.sun.tools.javac.main.Option.XLINT_CUSTOM.process(getOptionHelper(), "-Xlint:-options");
 513         } catch (com.sun.tools.javac.main.Option.InvalidValueException ignore) {
 514         }
 515 
 516         doclet.init(locale, messager);
 517         parseArgs(argList, javaNames);
 518 
 519         Arguments arguments = Arguments.instance(context);
 520         arguments.init(ProgramName);
 521         arguments.allowEmpty();
 522         if (!arguments.validate()) {
 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 (fileManager instanceof BaseFileManager) {
 532             ((BaseFileManager) fileManager).handleOptions(fileManagerOpts);
 533         }
 534 
 535         String platformString = compOpts.get("--release");
 536 
 537         if (platformString != null) {
 538             if (compOpts.isSet("-source")) {
 539                 String text = messager.getText("main.release.bootclasspath.conflict", "-source");
 540                 throw new ToolException(CMDERR, text);
 541             }
 542             if (fileManagerOpts.containsKey(BOOT_CLASS_PATH)) {
 543                 String text = messager.getText("main.release.bootclasspath.conflict",
 544                         BOOT_CLASS_PATH.getPrimaryName());
 545                 throw new ToolException(CMDERR, text);
 546             }
 547 
 548             PlatformDescription platformDescription =
 549                     PlatformUtils.lookupPlatformDescription(platformString);
 550 
 551             if (platformDescription == null) {
 552                 String text = messager.getText("main.unsupported.release.version", platformString);
 553                 throw new IllegalArgumentException(text);
 554             }
 555 
 556             compOpts.put(SOURCE, platformDescription.getSourceVersion());
 557 
 558             context.put(PlatformDescription.class, platformDescription);
 559 
 560             Collection<Path> platformCP = platformDescription.getPlatformPath();
 561 
 562             if (platformCP != null) {
 563                 if (fileManager instanceof StandardJavaFileManager) {
 564                     StandardJavaFileManager sfm = (StandardJavaFileManager) fileManager;
 565                     try {
 566                         sfm.setLocationFromPaths(StandardLocation.PLATFORM_CLASS_PATH, platformCP);
 567                     } catch (IOException ioe) {
 568                         throw new ToolException(SYSERR, ioe.getMessage(), ioe);
 569                     }
 570                 } else {
 571                     String text = messager.getText("main.release.not.standard.file.manager",
 572                                                     platformString);
 573                     throw new ToolException(ABNORMAL, text);
 574                 }
 575             }
 576         }
 577 
 578         compOpts.notifyListeners();
 579         List<String> modules = (List<String>) jdtoolOpts.computeIfAbsent(ToolOption.MODULE,
 580                 s -> Collections.EMPTY_LIST);
 581 
 582         if (modules.isEmpty()) {
 583             List<String> subpkgs = (List<String>) jdtoolOpts.computeIfAbsent(ToolOption.SUBPACKAGES,
 584                     s -> Collections.EMPTY_LIST);
 585             if (subpkgs.isEmpty()) {
 586                 if (javaNames.isEmpty() && isEmpty(fileObjects)) {
 587                     String text = messager.getText("main.No_modules_packages_or_classes_specified");
 588                     throw new ToolException(CMDERR, text);
 589                 }
 590             }
 591         }
 592 
 593         JavadocTool comp = JavadocTool.make0(context);
 594         if (comp == null) return ABNORMAL;
 595 
 596         DocletEnvironment docEnv = comp.getEnvironment(jdtoolOpts,
 597                 javaNames,
 598                 fileObjects);
 599 
 600         // release resources
 601         comp = null;
 602 
 603         if (breakiterator || !locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
 604             JavacTrees trees = JavacTrees.instance(context);
 605             trees.setBreakIterator(BreakIterator.getSentenceInstance(locale));
 606         }
 607         // pass off control to the doclet
 608         Result returnStatus = docEnv != null && doclet.run(docEnv)
 609                 ? OK
 610                 : ERROR;
 611 
 612         // We're done.
 613         if (compOpts.get("-verbose") != null) {
 614             tm = System.currentTimeMillis() - tm;
 615             messager.notice("main.done_in", Long.toString(tm));
 616         }
 617 
 618         return returnStatus;
 619     }
 620 
 621     boolean matches(List<String> names, String arg) {
 622         for (String name : names) {
 623             if (StringUtils.toLowerCase(name).equals(StringUtils.toLowerCase(arg)))
 624                 return true;
 625         }
 626         return false;
 627     }
 628 
 629     boolean matches(Doclet.Option option, String arg) {
 630         if (matches(option.getNames(), arg))
 631              return true;
 632         int sep = arg.indexOf(':');
 633         String targ = arg.substring(0, sep + 1);
 634         return matches(option.getNames(), targ);
 635     }
 636 
 637     Set<? extends Doclet.Option> docletOptions = null;
 638     int handleDocletOptions(int idx, List<String> args, boolean isToolOption)
 639             throws OptionException {
 640         if (docletOptions == null) {
 641             docletOptions = doclet.getSupportedOptions();
 642         }
 643         String arg = args.get(idx);
 644         String argBase, argVal;
 645         if (arg.startsWith("--") && arg.contains("=")) {
 646             int sep = arg.indexOf("=");
 647             argBase = arg.substring(0, sep);
 648             argVal = arg.substring(sep + 1);
 649         } else {
 650             argBase = arg;
 651             argVal = null;
 652         }
 653         String text = null;
 654         for (Doclet.Option opt : docletOptions) {
 655             if (matches(opt, argBase)) {
 656                 if (argVal != null) {
 657                     switch (opt.getArgumentCount()) {
 658                         case 0:
 659                             text = messager.getText("main.unnecessary_arg_provided", argBase);
 660                             throw new OptionException(ERROR, this::usage, text);
 661                         case 1:
 662                             opt.process(arg, Arrays.asList(argVal));
 663                             break;
 664                         default:
 665                             text = messager.getText("main.only_one_argument_with_equals", argBase);
 666                             throw new OptionException(ERROR, this::usage, text);
 667                     }
 668                 } else {
 669                     if (args.size() - idx -1 < opt.getArgumentCount()) {
 670                         text = messager.getText("main.requires_argument", arg);
 671                         throw new OptionException(ERROR, this::usage, text);
 672                     }
 673                     opt.process(arg, args.subList(idx + 1, args.size()));
 674                     idx += opt.getArgumentCount();
 675                 }
 676                 return idx;
 677             }
 678         }
 679         // check if arg is accepted by the tool before emitting error
 680         if (!isToolOption) {
 681             text = messager.getText("main.invalid_flag", arg);
 682             throw new OptionException(ERROR, this::usage, text);
 683         }
 684         return idx;
 685     }
 686 
 687     private Class<?> preprocess(JavaFileManager jfm,
 688             List<String> argv) throws ToolException, OptionException {
 689         // doclet specifying arguments
 690         String userDocletPath = null;
 691         String userDocletName = null;
 692 
 693         // taglet specifying arguments, since tagletpath is a doclet
 694         // functionality, assume they are repeated and inspect all.
 695         List<File> userTagletPath = new ArrayList<>();
 696         List<String> userTagletNames = new ArrayList<>();
 697 
 698         // Step 1: loop through the args, set locale early on, if found.
 699         for (int i = 0 ; i < argv.size() ; i++) {
 700             String arg = argv.get(i);
 701             if (arg.equals(ToolOption.DUMPONERROR.primaryName)) {
 702                 dumpOnError = true;
 703             } else if (arg.equals(ToolOption.LOCALE.primaryName)) {
 704                 checkOneArg(argv, i++);
 705                 String lname = argv.get(i);
 706                 locale = getLocale(lname);
 707             } else if (arg.equals(ToolOption.DOCLET.primaryName)) {
 708                 checkOneArg(argv, i++);
 709                 if (userDocletName != null) {
 710                     if (apiMode) {
 711                         throw new IllegalArgumentException("More than one doclet specified (" +
 712                                 userDocletName + " and " + argv.get(i) + ").");
 713                     }
 714                     String text = messager.getText("main.more_than_one_doclet_specified_0_and_1",
 715                             userDocletName, argv.get(i));
 716                     throw new ToolException(CMDERR, text);
 717                 }
 718                 if (docletName != null) {
 719                     if (apiMode) {
 720                         throw new IllegalArgumentException("More than one doclet specified (" +
 721                                 docletName + " and " + argv.get(i) + ").");
 722                     }
 723                     String text = messager.getText("main.more_than_one_doclet_specified_0_and_1",
 724                             docletName, argv.get(i));
 725                     throw new ToolException(CMDERR, text);
 726                 }
 727                 userDocletName = argv.get(i);
 728             } else if (arg.equals(ToolOption.DOCLETPATH.primaryName)) {
 729                 checkOneArg(argv, i++);
 730                 if (userDocletPath == null) {
 731                     userDocletPath = argv.get(i);
 732                 } else {
 733                     userDocletPath += File.pathSeparator + argv.get(i);
 734                 }
 735             } else if ("-taglet".equals(arg)) {
 736                 userTagletNames.add(argv.get(i + 1));
 737             } else if ("-tagletpath".equals(arg)) {
 738                 for (String pathname : argv.get(i + 1).split(File.pathSeparator)) {
 739                     userTagletPath.add(new File(pathname));
 740                 }
 741             }
 742         }
 743 
 744         // Step 2: a doclet is provided, nothing more to do.
 745         if (docletClass != null) {
 746             return docletClass;
 747         }
 748 
 749         // Step 3: doclet name specified ? if so find a ClassLoader,
 750         // and load it.
 751         if (userDocletName != null) {
 752             ClassLoader cl = classLoader;
 753             if (cl == null) {
 754                 if (!fileManager.hasLocation(DOCLET_PATH)) {
 755                     List<File> paths = new ArrayList<>();
 756                     if (userDocletPath != null) {
 757                         for (String pathname : userDocletPath.split(File.pathSeparator)) {
 758                             paths.add(new File(pathname));
 759                         }
 760                     }
 761                     try {
 762                         ((StandardJavaFileManager)fileManager).setLocation(DOCLET_PATH, paths);
 763                     } catch (IOException ioe) {
 764                         if (apiMode) {
 765                             throw new IllegalArgumentException("Could not set location for " +
 766                                     userDocletPath, ioe);
 767                         }
 768                         String text = messager.getText("main.doclet_could_not_set_location",
 769                                 userDocletPath);
 770                         throw new ToolException(CMDERR, text, ioe);
 771                     }
 772                 }
 773                 cl = fileManager.getClassLoader(DOCLET_PATH);
 774                 if (cl == null) {
 775                     // despite doclet specified on cmdline no classloader found!
 776                     if (apiMode) {
 777                         throw new IllegalArgumentException("Could not obtain classloader to load "
 778                                 + userDocletPath);
 779                     }
 780                     String text = messager.getText("main.doclet_no_classloader_found",
 781                             userDocletName);
 782                     throw new ToolException(CMDERR, text);
 783                 }
 784             }
 785             try {
 786                 Class<?> klass = cl.loadClass(userDocletName);
 787                 return klass;
 788             } catch (ClassNotFoundException cnfe) {
 789                 if (apiMode) {
 790                     throw new IllegalArgumentException("Cannot find doclet class " + userDocletName,
 791                             cnfe);
 792                 }
 793                 String text = messager.getText("main.doclet_class_not_found", userDocletName);
 794                 throw new ToolException(CMDERR, text, cnfe);
 795             }
 796         }
 797 
 798         // Step 4: we have a doclet, try loading it
 799         if (docletName != null) {
 800             try {
 801                 return Class.forName(docletName, true, getClass().getClassLoader());
 802             } catch (ClassNotFoundException cnfe) {
 803                 if (apiMode) {
 804                     throw new IllegalArgumentException("Cannot find doclet class " + userDocletName);
 805                 }
 806                 String text = messager.getText("main.doclet_class_not_found", userDocletName);
 807                 throw new ToolException(CMDERR, text, cnfe);
 808             }
 809         }
 810 
 811         // Step 5: we don't have a doclet specified, do we have taglets ?
 812         if (!userTagletNames.isEmpty() && hasOldTaglet(userTagletNames, userTagletPath)) {
 813             // found a bogey, return the old doclet
 814             return OldStdDoclet;
 815         }
 816 
 817         // finally
 818         return StdDoclet;
 819     }
 820 
 821     /*
 822      * This method returns true iff it finds a legacy taglet, but for
 823      * all other conditions including errors it returns false, allowing
 824      * nature to take its own course.
 825      */
 826     @SuppressWarnings("deprecation")
 827     private boolean hasOldTaglet(List<String> tagletNames, List<File> tagletPaths) throws ToolException {
 828         if (!fileManager.hasLocation(TAGLET_PATH)) {
 829             try {
 830                 ((StandardJavaFileManager) fileManager).setLocation(TAGLET_PATH, tagletPaths);
 831             } catch (IOException ioe) {
 832                 String text = messager.getText("main.doclet_could_not_set_location", tagletPaths);
 833                 throw new ToolException(CMDERR, text, ioe);
 834             }
 835         }
 836         ClassLoader cl = fileManager.getClassLoader(TAGLET_PATH);
 837         if (cl == null) {
 838             // no classloader found!
 839             String text = messager.getText("main.doclet_no_classloader_found", tagletNames.get(0));
 840             throw new ToolException(CMDERR, text);
 841         }
 842         for (String tagletName : tagletNames) {
 843             try {
 844                 Class<?> klass = cl.loadClass(tagletName);
 845                 if (com.sun.tools.doclets.Taglet.class.isAssignableFrom(klass)) {
 846                     return true;
 847                 }
 848             } catch (ClassNotFoundException cnfe) {
 849                 String text = messager.getText("main.doclet_class_not_found", tagletName);
 850                 throw new ToolException(CMDERR, text, cnfe);
 851             }
 852         }
 853         return false;
 854     }
 855 
 856     private void parseArgs(List<String> args, List<String> javaNames) throws ToolException,
 857             OptionException, com.sun.tools.javac.main.Option.InvalidValueException {
 858         for (int i = 0 ; i < args.size() ; i++) {
 859             String arg = args.get(i);
 860             ToolOption o = ToolOption.get(arg);
 861             if (o != null) {
 862                 // handle a doclet argument that may be needed however
 863                 // don't increment the index, and allow the tool to consume args
 864                 handleDocletOptions(i, args, true);
 865                 if (o.hasArg) {
 866                     if (arg.startsWith("--") && arg.contains("=")) {
 867                         o.process(this, arg.substring(arg.indexOf('=') + 1));
 868                     } else {
 869                         checkOneArg(args, i++);
 870                         o.process(this, args.get(i));
 871                     }
 872                 } else if (o.hasSuffix) {
 873                     o.process(this, arg);
 874                 } else {
 875                     o.process(this);
 876                 }
 877             } else if (arg.startsWith("-XD")) {
 878                 // hidden javac options
 879                 String s = arg.substring("-XD".length());
 880                 int eq = s.indexOf('=');
 881                 String key = (eq < 0) ? s : s.substring(0, eq);
 882                 String value = (eq < 0) ? s : s.substring(eq+1);
 883                 compOpts.put(key, value);
 884             } else if (arg.startsWith("-")) {
 885                 i = handleDocletOptions(i, args, false);
 886             } else {
 887                 javaNames.add(arg);
 888             }
 889         }
 890     }
 891 
 892     private <T> boolean isEmpty(Iterable<T> iter) {
 893         return !iter.iterator().hasNext();
 894     }
 895 
 896     /**
 897      * Check the one arg option.
 898      * Error and exit if one argument is not provided.
 899      */
 900     private void checkOneArg(List<String> args, int index) throws OptionException {
 901         if ((index + 1) >= args.size() || args.get(index + 1).startsWith("-d")) {
 902             String text = messager.getText("main.requires_argument", args.get(index));
 903             throw new OptionException(CMDERR, this::usage, text);
 904         }
 905     }
 906 
 907     void error(String key, Object... args) {
 908         messager.printErrorUsingKey(key, args);
 909     }
 910 
 911     void warn(String key, Object... args)  {
 912         messager.printWarningUsingKey(key, args);
 913     }
 914 
 915     /**
 916      * Get the locale if specified on the command line
 917      * else return null and if locale option is not used
 918      * then return default locale.
 919      */
 920     private Locale getLocale(String localeName) throws ToolException {
 921         Locale userlocale = null;
 922         if (localeName == null || localeName.isEmpty()) {
 923             return Locale.getDefault();
 924         }
 925         int firstuscore = localeName.indexOf('_');
 926         int seconduscore = -1;
 927         String language = null;
 928         String country = null;
 929         String variant = null;
 930         if (firstuscore == 2) {
 931             language = localeName.substring(0, firstuscore);
 932             seconduscore = localeName.indexOf('_', firstuscore + 1);
 933             if (seconduscore > 0) {
 934                 if (seconduscore != firstuscore + 3
 935                         || localeName.length() <= seconduscore + 1) {
 936                     String text = messager.getText("main.malformed_locale_name", localeName);
 937                     throw new ToolException(CMDERR, text);
 938                 }
 939                 country = localeName.substring(firstuscore + 1,
 940                         seconduscore);
 941                 variant = localeName.substring(seconduscore + 1);
 942             } else if (localeName.length() == firstuscore + 3) {
 943                 country = localeName.substring(firstuscore + 1);
 944             } else {
 945                 String text = messager.getText("main.malformed_locale_name", localeName);
 946                 throw new ToolException(CMDERR, text);
 947             }
 948         } else if (firstuscore == -1 && localeName.length() == 2) {
 949             language = localeName;
 950         } else {
 951             String text = messager.getText("main.malformed_locale_name", localeName);
 952             throw new ToolException(CMDERR, text);
 953         }
 954         userlocale = searchLocale(language, country, variant);
 955         if (userlocale == null) {
 956             String text = messager.getText("main.illegal_locale_name", localeName);
 957             throw new ToolException(CMDERR, text);
 958         } else {
 959             return userlocale;
 960         }
 961     }
 962 
 963     /**
 964      * Search the locale for specified language, specified country and
 965      * specified variant.
 966      */
 967     private Locale searchLocale(String language, String country,
 968                                 String variant) {
 969         for (Locale loc : Locale.getAvailableLocales()) {
 970             if (loc.getLanguage().equals(language) &&
 971                 (country == null || loc.getCountry().equals(country)) &&
 972                 (variant == null || loc.getVariant().equals(variant))) {
 973                 return loc;
 974             }
 975         }
 976         return null;
 977     }
 978 
 979     @Override
 980     OptionHelper getOptionHelper() {
 981         return new GrumpyHelper(messager) {
 982             @Override
 983             public String get(com.sun.tools.javac.main.Option option) {
 984                 return compOpts.get(option);
 985             }
 986 
 987             @Override
 988             public void put(String name, String value) {
 989                 compOpts.put(name, value);
 990             }
 991 
 992             @Override
 993             public void remove(String name) {
 994                 compOpts.remove(name);
 995             }
 996 
 997             @Override
 998             public boolean handleFileManagerOption(com.sun.tools.javac.main.Option option, String value) {
 999                 fileManagerOpts.put(option, value);
1000                 return true;
1001             }
1002         };
1003     }
1004 
1005     @Override
1006     String getLocalizedMessage(String msg, Object... args) {
1007         return messager.getText(msg, args);
1008     }
1009 }