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     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         doclet.init(locale, messager);
 516         parseArgs(argList, javaNames);
 517 
 518         Arguments arguments = Arguments.instance(context);
 519         arguments.init(ProgramName);
 520         arguments.allowEmpty();
 521         if (!arguments.validate()) {
 522             // Arguments does not always increase the error count in the
 523             // case of errors, so increment the error count only if it has
 524             // not been updated previously, preventing complaints by callers
 525             if (!messager.hasErrors() && !messager.hasWarnings())
 526                 messager.nerrors++;
 527             return CMDERR;
 528         }
 529 
 530         if (fileManager instanceof BaseFileManager) {
 531             ((BaseFileManager) fileManager).handleOptions(fileManagerOpts);
 532         }
 533 
 534         String platformString = compOpts.get("--release");
 535 
 536         if (platformString != null) {
 537             if (compOpts.isSet("-source")) {
 538                 String text = messager.getText("main.release.bootclasspath.conflict", "-source");
 539                 throw new ToolException(CMDERR, text);
 540             }
 541             if (fileManagerOpts.containsKey(BOOT_CLASS_PATH)) {
 542                 String text = messager.getText("main.release.bootclasspath.conflict",
 543                         BOOT_CLASS_PATH.getPrimaryName());
 544                 throw new ToolException(CMDERR, text);
 545             }
 546 
 547             PlatformDescription platformDescription =
 548                     PlatformUtils.lookupPlatformDescription(platformString);
 549 
 550             if (platformDescription == null) {
 551                 String text = messager.getText("main.unsupported.release.version", platformString);
 552                 throw new IllegalArgumentException(text);
 553             }
 554 
 555             compOpts.put(SOURCE, platformDescription.getSourceVersion());
 556 
 557             context.put(PlatformDescription.class, platformDescription);
 558 
 559             Collection<Path> platformCP = platformDescription.getPlatformPath();
 560 
 561             if (platformCP != null) {
 562                 if (fileManager instanceof StandardJavaFileManager) {
 563                     StandardJavaFileManager sfm = (StandardJavaFileManager) fileManager;
 564                     try {
 565                         sfm.setLocationFromPaths(StandardLocation.PLATFORM_CLASS_PATH, platformCP);
 566                     } catch (IOException ioe) {
 567                         throw new ToolException(SYSERR, ioe.getMessage(), ioe);
 568                     }
 569                 } else {
 570                     String text = messager.getText("main.release.not.standard.file.manager",
 571                                                     platformString);
 572                     throw new ToolException(ABNORMAL, text);
 573                 }
 574             }
 575         }
 576 
 577         compOpts.notifyListeners();
 578         List<String> modules = (List<String>) jdtoolOpts.computeIfAbsent(ToolOption.MODULE,
 579                 s -> Collections.EMPTY_LIST);
 580 
 581         if (modules.isEmpty()) {
 582             List<String> subpkgs = (List<String>) jdtoolOpts.computeIfAbsent(ToolOption.SUBPACKAGES,
 583                     s -> Collections.EMPTY_LIST);
 584             if (subpkgs.isEmpty()) {
 585                 if (javaNames.isEmpty() && isEmpty(fileObjects)) {
 586                     String text = messager.getText("main.No_modules_packages_or_classes_specified");
 587                     throw new ToolException(CMDERR, text);
 588                 }
 589             }
 590         }
 591 
 592         JavadocTool comp = JavadocTool.make0(context);
 593         if (comp == null) return ABNORMAL;
 594 
 595         DocletEnvironment docEnv = comp.getEnvironment(jdtoolOpts,
 596                 javaNames,
 597                 fileObjects);
 598 
 599         // release resources
 600         comp = null;
 601 
 602         if (breakiterator || !locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
 603             JavacTrees trees = JavacTrees.instance(context);
 604             trees.setBreakIterator(BreakIterator.getSentenceInstance(locale));
 605         }
 606         // pass off control to the doclet
 607         Result returnStatus = docEnv != null && doclet.run(docEnv)
 608                 ? OK
 609                 : ERROR;
 610 
 611         // We're done.
 612         if (compOpts.get("-verbose") != null) {
 613             tm = System.currentTimeMillis() - tm;
 614             messager.notice("main.done_in", Long.toString(tm));
 615         }
 616 
 617         return returnStatus;
 618     }
 619 
 620     boolean matches(List<String> names, String arg) {
 621         for (String name : names) {
 622             if (StringUtils.toLowerCase(name).equals(StringUtils.toLowerCase(arg)))
 623                 return true;
 624         }
 625         return false;
 626     }
 627 
 628     boolean matches(Doclet.Option option, String arg) {
 629         if (matches(option.getNames(), arg))
 630              return true;
 631         int sep = arg.indexOf(':');
 632         String targ = arg.substring(0, sep + 1);
 633         return matches(option.getNames(), targ);
 634     }
 635 
 636     Set<? extends Doclet.Option> docletOptions = null;
 637     int handleDocletOptions(int idx, List<String> args, boolean isToolOption)
 638             throws OptionException {
 639         if (docletOptions == null) {
 640             docletOptions = doclet.getSupportedOptions();
 641         }
 642         String arg = args.get(idx);
 643         String argBase, argVal;
 644         if (arg.startsWith("--") && arg.contains("=")) {
 645             int sep = arg.indexOf("=");
 646             argBase = arg.substring(0, sep);
 647             argVal = arg.substring(sep + 1);
 648         } else {
 649             argBase = arg;
 650             argVal = null;
 651         }
 652         String text = null;
 653         for (Doclet.Option opt : docletOptions) {
 654             if (matches(opt, argBase)) {
 655                 if (argVal != null) {
 656                     switch (opt.getArgumentCount()) {
 657                         case 0:
 658                             text = messager.getText("main.unnecessary_arg_provided", argBase);
 659                             throw new OptionException(ERROR, this::usage, text);
 660                         case 1:
 661                             opt.process(arg, Arrays.asList(argVal));
 662                             break;
 663                         default:
 664                             text = messager.getText("main.only_one_argument_with_equals", argBase);
 665                             throw new OptionException(ERROR, this::usage, text);
 666                     }
 667                 } else {
 668                     if (args.size() - idx -1 < opt.getArgumentCount()) {
 669                         text = messager.getText("main.requires_argument", arg);
 670                         throw new OptionException(ERROR, this::usage, text);
 671                     }
 672                     opt.process(arg, args.subList(idx + 1, args.size()));
 673                     idx += opt.getArgumentCount();
 674                 }
 675                 return idx;
 676             }
 677         }
 678         // check if arg is accepted by the tool before emitting error
 679         if (!isToolOption) {
 680             text = messager.getText("main.invalid_flag", arg);
 681             throw new OptionException(ERROR, this::usage, text);
 682         }
 683         return idx;
 684     }
 685 
 686     private Class<?> preprocess(JavaFileManager jfm,
 687             List<String> argv) throws ToolException, OptionException {
 688         // doclet specifying arguments
 689         String userDocletPath = null;
 690         String userDocletName = null;
 691 
 692         // taglet specifying arguments, since tagletpath is a doclet
 693         // functionality, assume they are repeated and inspect all.
 694         List<File> userTagletPath = new ArrayList<>();
 695         List<String> userTagletNames = new ArrayList<>();
 696 
 697         // Step 1: loop through the args, set locale early on, if found.
 698         for (int i = 0 ; i < argv.size() ; i++) {
 699             String arg = argv.get(i);
 700             if (arg.equals(ToolOption.DUMPONERROR.primaryName)) {
 701                 dumpOnError = true;
 702             } else if (arg.equals(ToolOption.LOCALE.primaryName)) {
 703                 checkOneArg(argv, i++);
 704                 String lname = argv.get(i);
 705                 locale = getLocale(lname);
 706             } else if (arg.equals(ToolOption.DOCLET.primaryName)) {
 707                 checkOneArg(argv, i++);
 708                 if (userDocletName != null) {
 709                     if (apiMode) {
 710                         throw new IllegalArgumentException("More than one doclet specified (" +
 711                                 userDocletName + " and " + argv.get(i) + ").");
 712                     }
 713                     String text = messager.getText("main.more_than_one_doclet_specified_0_and_1",
 714                             userDocletName, argv.get(i));
 715                     throw new ToolException(CMDERR, text);
 716                 }
 717                 if (docletName != null) {
 718                     if (apiMode) {
 719                         throw new IllegalArgumentException("More than one doclet specified (" +
 720                                 docletName + " and " + argv.get(i) + ").");
 721                     }
 722                     String text = messager.getText("main.more_than_one_doclet_specified_0_and_1",
 723                             docletName, argv.get(i));
 724                     throw new ToolException(CMDERR, text);
 725                 }
 726                 userDocletName = argv.get(i);
 727             } else if (arg.equals(ToolOption.DOCLETPATH.primaryName)) {
 728                 checkOneArg(argv, i++);
 729                 if (userDocletPath == null) {
 730                     userDocletPath = argv.get(i);
 731                 } else {
 732                     userDocletPath += File.pathSeparator + argv.get(i);
 733                 }
 734             } else if ("-taglet".equals(arg)) {
 735                 userTagletNames.add(argv.get(i + 1));
 736             } else if ("-tagletpath".equals(arg)) {
 737                 for (String pathname : argv.get(i + 1).split(File.pathSeparator)) {
 738                     userTagletPath.add(new File(pathname));
 739                 }
 740             }
 741         }
 742 
 743         // Step 2: a doclet is provided, nothing more to do.
 744         if (docletClass != null) {
 745             return docletClass;
 746         }
 747 
 748         // Step 3: doclet name specified ? if so find a ClassLoader,
 749         // and load it.
 750         if (userDocletName != null) {
 751             ClassLoader cl = classLoader;
 752             if (cl == null) {
 753                 if (!fileManager.hasLocation(DOCLET_PATH)) {
 754                     List<File> paths = new ArrayList<>();
 755                     if (userDocletPath != null) {
 756                         for (String pathname : userDocletPath.split(File.pathSeparator)) {
 757                             paths.add(new File(pathname));
 758                         }
 759                     }
 760                     try {
 761                         ((StandardJavaFileManager)fileManager).setLocation(DOCLET_PATH, paths);
 762                     } catch (IOException ioe) {
 763                         if (apiMode) {
 764                             throw new IllegalArgumentException("Could not set location for " +
 765                                     userDocletPath, ioe);
 766                         }
 767                         String text = messager.getText("main.doclet_could_not_set_location",
 768                                 userDocletPath);
 769                         throw new ToolException(CMDERR, text, ioe);
 770                     }
 771                 }
 772                 cl = fileManager.getClassLoader(DOCLET_PATH);
 773                 if (cl == null) {
 774                     // despite doclet specified on cmdline no classloader found!
 775                     if (apiMode) {
 776                         throw new IllegalArgumentException("Could not obtain classloader to load "
 777                                 + userDocletPath);
 778                     }
 779                     String text = messager.getText("main.doclet_no_classloader_found",
 780                             userDocletName);
 781                     throw new ToolException(CMDERR, text);
 782                 }
 783             }
 784             try {
 785                 Class<?> klass = cl.loadClass(userDocletName);
 786                 return klass;
 787             } catch (ClassNotFoundException cnfe) {
 788                 if (apiMode) {
 789                     throw new IllegalArgumentException("Cannot find doclet class " + userDocletName,
 790                             cnfe);
 791                 }
 792                 String text = messager.getText("main.doclet_class_not_found", userDocletName);
 793                 throw new ToolException(CMDERR, text, cnfe);
 794             }
 795         }
 796 
 797         // Step 4: we have a doclet, try loading it
 798         if (docletName != null) {
 799             return loadDocletClass(docletName);
 800         }
 801 
 802         // Step 5: we don't have a doclet specified, do we have taglets ?
 803         if (!userTagletNames.isEmpty() && hasOldTaglet(userTagletNames, userTagletPath)) {
 804             // found a bogey, return the old doclet
 805             return loadDocletClass(OldStdDocletName);
 806         }
 807 
 808         // finally
 809         return StdDoclet;
 810     }
 811 
 812     private Class<?> loadDocletClass(String docletName) throws ToolException {
 813         try {
 814             return Class.forName(docletName, true, getClass().getClassLoader());
 815         } catch (ClassNotFoundException cnfe) {
 816             if (apiMode) {
 817                 throw new IllegalArgumentException("Cannot find doclet class " + docletName);
 818             }
 819             String text = messager.getText("main.doclet_class_not_found", docletName);
 820             throw new ToolException(CMDERR, text, cnfe);
 821         }
 822     }
 823 
 824     /*
 825      * This method returns true iff it finds a legacy taglet, but for
 826      * all other conditions including errors it returns false, allowing
 827      * nature to take its own course.
 828      */
 829     @SuppressWarnings("deprecation")
 830     private boolean hasOldTaglet(List<String> tagletNames, List<File> tagletPaths) throws ToolException {
 831         if (!fileManager.hasLocation(TAGLET_PATH)) {
 832             try {
 833                 ((StandardJavaFileManager) fileManager).setLocation(TAGLET_PATH, tagletPaths);
 834             } catch (IOException ioe) {
 835                 String text = messager.getText("main.doclet_could_not_set_location", tagletPaths);
 836                 throw new ToolException(CMDERR, text, ioe);
 837             }
 838         }
 839         ClassLoader cl = fileManager.getClassLoader(TAGLET_PATH);
 840         if (cl == null) {
 841             // no classloader found!
 842             String text = messager.getText("main.doclet_no_classloader_found", tagletNames.get(0));
 843             throw new ToolException(CMDERR, text);
 844         }
 845         for (String tagletName : tagletNames) {
 846             try {
 847                 Class<?> klass = cl.loadClass(tagletName);
 848                 if (com.sun.tools.doclets.Taglet.class.isAssignableFrom(klass)) {
 849                     return true;
 850                 }
 851             } catch (ClassNotFoundException cnfe) {
 852                 String text = messager.getText("main.doclet_class_not_found", tagletName);
 853                 throw new ToolException(CMDERR, text, cnfe);
 854             }
 855         }
 856         return false;
 857     }
 858 
 859     private void parseArgs(List<String> args, List<String> javaNames) throws ToolException,
 860             OptionException, com.sun.tools.javac.main.Option.InvalidValueException {
 861         for (int i = 0 ; i < args.size() ; i++) {
 862             String arg = args.get(i);
 863             ToolOption o = ToolOption.get(arg);
 864             if (o != null) {
 865                 // handle a doclet argument that may be needed however
 866                 // don't increment the index, and allow the tool to consume args
 867                 handleDocletOptions(i, args, true);
 868                 if (o.hasArg) {
 869                     if (arg.startsWith("--") && arg.contains("=")) {
 870                         o.process(this, arg.substring(arg.indexOf('=') + 1));
 871                     } else {
 872                         checkOneArg(args, i++);
 873                         o.process(this, args.get(i));
 874                     }
 875                 } else if (o.hasSuffix) {
 876                     o.process(this, arg);
 877                 } else {
 878                     o.process(this);
 879                 }
 880             } else if (arg.startsWith("-XD")) {
 881                 // hidden javac options
 882                 String s = arg.substring("-XD".length());
 883                 int eq = s.indexOf('=');
 884                 String key = (eq < 0) ? s : s.substring(0, eq);
 885                 String value = (eq < 0) ? s : s.substring(eq+1);
 886                 compOpts.put(key, value);
 887             } else if (arg.startsWith("-")) {
 888                 i = handleDocletOptions(i, args, false);
 889             } else {
 890                 javaNames.add(arg);
 891             }
 892         }
 893     }
 894 
 895     private <T> boolean isEmpty(Iterable<T> iter) {
 896         return !iter.iterator().hasNext();
 897     }
 898 
 899     /**
 900      * Check the one arg option.
 901      * Error and exit if one argument is not provided.
 902      */
 903     private void checkOneArg(List<String> args, int index) throws OptionException {
 904         if ((index + 1) >= args.size() || args.get(index + 1).startsWith("-d")) {
 905             String text = messager.getText("main.requires_argument", args.get(index));
 906             throw new OptionException(CMDERR, this::usage, text);
 907         }
 908     }
 909 
 910     void error(String key, Object... args) {
 911         messager.printErrorUsingKey(key, args);
 912     }
 913 
 914     void warn(String key, Object... args)  {
 915         messager.printWarningUsingKey(key, args);
 916     }
 917 
 918     /**
 919      * Get the locale if specified on the command line
 920      * else return null and if locale option is not used
 921      * then return default locale.
 922      */
 923     private Locale getLocale(String localeName) throws ToolException {
 924         Locale userlocale = null;
 925         if (localeName == null || localeName.isEmpty()) {
 926             return Locale.getDefault();
 927         }
 928         int firstuscore = localeName.indexOf('_');
 929         int seconduscore = -1;
 930         String language = null;
 931         String country = null;
 932         String variant = null;
 933         if (firstuscore == 2) {
 934             language = localeName.substring(0, firstuscore);
 935             seconduscore = localeName.indexOf('_', firstuscore + 1);
 936             if (seconduscore > 0) {
 937                 if (seconduscore != firstuscore + 3
 938                         || localeName.length() <= seconduscore + 1) {
 939                     String text = messager.getText("main.malformed_locale_name", localeName);
 940                     throw new ToolException(CMDERR, text);
 941                 }
 942                 country = localeName.substring(firstuscore + 1,
 943                         seconduscore);
 944                 variant = localeName.substring(seconduscore + 1);
 945             } else if (localeName.length() == firstuscore + 3) {
 946                 country = localeName.substring(firstuscore + 1);
 947             } else {
 948                 String text = messager.getText("main.malformed_locale_name", localeName);
 949                 throw new ToolException(CMDERR, text);
 950             }
 951         } else if (firstuscore == -1 && localeName.length() == 2) {
 952             language = localeName;
 953         } else {
 954             String text = messager.getText("main.malformed_locale_name", localeName);
 955             throw new ToolException(CMDERR, text);
 956         }
 957         userlocale = searchLocale(language, country, variant);
 958         if (userlocale == null) {
 959             String text = messager.getText("main.illegal_locale_name", localeName);
 960             throw new ToolException(CMDERR, text);
 961         } else {
 962             return userlocale;
 963         }
 964     }
 965 
 966     /**
 967      * Search the locale for specified language, specified country and
 968      * specified variant.
 969      */
 970     private Locale searchLocale(String language, String country,
 971                                 String variant) {
 972         for (Locale loc : Locale.getAvailableLocales()) {
 973             if (loc.getLanguage().equals(language) &&
 974                 (country == null || loc.getCountry().equals(country)) &&
 975                 (variant == null || loc.getVariant().equals(variant))) {
 976                 return loc;
 977             }
 978         }
 979         return null;
 980     }
 981 
 982     @Override
 983     OptionHelper getOptionHelper() {
 984         return new GrumpyHelper(messager) {
 985             @Override
 986             public String get(com.sun.tools.javac.main.Option option) {
 987                 return compOpts.get(option);
 988             }
 989 
 990             @Override
 991             public void put(String name, String value) {
 992                 compOpts.put(name, value);
 993             }
 994 
 995             @Override
 996             public void remove(String name) {
 997                 compOpts.remove(name);
 998             }
 999 
1000             @Override
1001             public boolean handleFileManagerOption(com.sun.tools.javac.main.Option option, String value) {
1002                 fileManagerOpts.put(option, value);
1003                 return true;
1004             }
1005         };
1006     }
1007 
1008     @Override
1009     String getLocalizedMessage(String msg, Object... args) {
1010         return messager.getText(msg, args);
1011     }
1012 }