Start.java revision 3233:b5d08bc0d224
1/*
2 * Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.javadoc.internal.tool;
27
28import java.io.File;
29import java.io.FileNotFoundException;
30import java.io.IOException;
31import java.io.PrintWriter;
32import java.nio.file.Path;
33import java.text.BreakIterator;
34import java.util.ArrayList;
35import java.util.Arrays;
36import java.util.Collection;
37import java.util.Collections;
38import java.util.List;
39import java.util.Locale;
40import java.util.Objects;
41import java.util.Set;
42import static javax.tools.DocumentationTool.Location.*;
43import javax.tools.JavaFileManager;
44import javax.tools.JavaFileObject;
45import javax.tools.StandardJavaFileManager;
46import javax.tools.StandardLocation;
47
48import com.sun.tools.javac.api.JavacTrees;
49import com.sun.tools.javac.file.BaseFileManager;
50import com.sun.tools.javac.file.JavacFileManager;
51import com.sun.tools.javac.main.CommandLine;
52import com.sun.tools.javac.platform.PlatformDescription;
53import com.sun.tools.javac.platform.PlatformUtils;
54import com.sun.tools.javac.util.ClientCodeException;
55import com.sun.tools.javac.util.Context;
56import com.sun.tools.javac.util.Log;
57import com.sun.tools.javac.util.Options;
58
59import jdk.javadoc.doclet.Doclet;
60import jdk.javadoc.doclet.Doclet.Option;
61import jdk.javadoc.doclet.DocletEnvironment;
62
63import static com.sun.tools.javac.main.Option.*;
64/**
65 * Main program of Javadoc.
66 * Previously named "Main".
67 *
68 *  <p><b>This is NOT part of any supported API.
69 *  If you write code that depends on this, you do so at your own risk.
70 *  This code and its internal interfaces are subject to change or
71 *  deletion without notice.</b>
72 *
73 * @author Robert Field
74 * @author Neal Gafter (rewrite)
75 */
76public class Start extends ToolOption.Helper {
77    /** Context for this invocation. */
78    private final Context context;
79
80    private static final String ProgramName = "javadoc";
81
82    // meaning we allow all visibility of PROTECTED and PUBLIC
83    private static final String defaultModifier = "protected";
84
85    private Messager messager;
86
87    private final String docletName;
88
89    private final ClassLoader classLoader;
90
91    private Class<?> docletClass;
92
93    private Doclet doclet;
94
95    // used to determine the locale for the messager
96    private Locale locale;
97
98
99    /**
100     * In API mode, exceptions thrown while calling the doclet are
101     * propagated using ClientCodeException.
102     */
103    private boolean apiMode;
104
105    private JavaFileManager fileManager;
106
107    Start() {
108        this(null, null, null, null, null);
109    }
110
111    Start(PrintWriter writer) {
112        this(null, null, writer, null, null);
113    }
114
115    Start(Context context, String programName, PrintWriter writer,
116            String docletName, ClassLoader classLoader) {
117        this.context = context == null ? new Context() : context;
118        String pname = programName == null ? ProgramName : programName;
119        this.messager = writer == null
120                ? new Messager(this.context, pname)
121                : new Messager(this.context, pname, writer, writer);
122        this.docletName = docletName;
123        this.classLoader = classLoader;
124        this.docletClass = null;
125        this.locale = Locale.getDefault();
126    }
127
128    public Start(Context context) {
129        this.docletClass = null;
130        this.context = Objects.requireNonNull(context);
131        this.apiMode = true;
132        this.docletName = null;
133        this.classLoader = null;
134        this.locale = Locale.getDefault();
135    }
136
137    void initMessager() {
138        if (!apiMode)
139            return;
140        if (messager == null) {
141            Log log = context.get(Log.logKey);
142            if (log instanceof Messager) {
143                messager = (Messager) log;
144            } else {
145                PrintWriter out = context.get(Log.outKey);
146                messager = (out == null)
147                        ? new Messager(context, ProgramName)
148                        : new Messager(context, ProgramName, out, out);
149            }
150        }
151    }
152
153    /**
154     * Usage
155     */
156    @Override
157    void usage() {
158        usage(true);
159    }
160
161    void usage(boolean exit) {
162        usage("main.usage", "-help", null, exit);
163    }
164
165    @Override
166    void Xusage() {
167        Xusage(true);
168    }
169
170    void Xusage(boolean exit) {
171        usage("main.Xusage", "-X", "main.Xusage.foot", exit);
172    }
173
174    private void usage(String main, String option, String foot, boolean exit) {
175        messager.notice(main);
176        // let doclet print usage information (does nothing on error)
177        if (docletClass != null) {
178            String name = doclet.getName();
179            Set<Option> supportedOptions = doclet.getSupportedOptions();
180            messager.notice("main.doclet.usage.header", name);
181            Option.Kind myKind = option.equals("-X")
182                    ? Option.Kind.EXTENDED
183                    : Option.Kind.STANDARD;
184            supportedOptions.stream()
185                    .filter(opt -> opt.getKind() == myKind)
186                    .forEach(opt -> messager.printNotice(opt.toString()));
187        }
188        if (foot != null)
189            messager.notice(foot);
190
191        if (exit) exit();
192    }
193
194    /**
195     * Exit
196     */
197    private void exit() {
198        messager.exit();
199    }
200
201    /**
202     * Main program - external wrapper
203     */
204    int begin(String... argv) {
205        // Preprocess @file arguments
206        try {
207            argv = CommandLine.parse(argv);
208        } catch (FileNotFoundException e) {
209            messager.error("main.cant.read", e.getMessage());
210            exit();
211        } catch (IOException e) {
212            e.printStackTrace(System.err);
213            exit();
214        }
215
216        List<String> argList = Arrays.asList(argv);
217        boolean ok = begin(argList, Collections.<JavaFileObject> emptySet());
218        return ok ? 0 : 1;
219    }
220
221    // Called by 199 API.
222    public boolean begin(Class<?> docletClass,
223            Iterable<String> options,
224            Iterable<? extends JavaFileObject> fileObjects) {
225        this.docletClass = docletClass;
226        List<String> opts = new ArrayList<>();
227        for (String opt: options)
228            opts.add(opt);
229        return begin(opts, fileObjects);
230    }
231
232    private boolean begin(List<String> options, Iterable<? extends JavaFileObject> fileObjects) {
233
234        fileManager = context.get(JavaFileManager.class);
235        if (fileManager == null) {
236            JavacFileManager.preRegister(context);
237            fileManager = context.get(JavaFileManager.class);
238            if (fileManager instanceof BaseFileManager) {
239                ((BaseFileManager) fileManager).autoClose = true;
240            }
241        }
242        // locale and doclet needs to be determined first
243        docletClass = preProcess(fileManager, options);
244
245        if (jdk.javadoc.doclet.Doclet.class.isAssignableFrom(docletClass)) {
246            // no need to dispatch to old, safe to init now
247            initMessager();
248            messager.setLocale(locale);
249            try {
250                doclet = (Doclet) docletClass.newInstance();
251            } catch (InstantiationException | IllegalAccessException exc) {
252                if (!apiMode) {
253                    error("main.could_not_instantiate_class", docletClass);
254                    messager.exit();
255                }
256                throw new ClientCodeException(exc);
257            }
258        } else {
259            if (this.apiMode) {
260                com.sun.tools.javadoc.Start ostart
261                        = new com.sun.tools.javadoc.Start(context);
262                return ostart.begin(docletClass, options, fileObjects);
263            }
264            String[] array = options.toArray(new String[options.size()]);
265            return com.sun.tools.javadoc.Main.execute(array) == 0;
266        }
267
268        boolean failed = false;
269        try {
270            failed = !parseAndExecute(options, fileObjects);
271        } catch (Messager.ExitJavadoc exc) {
272            // ignore, we just exit this way
273        } catch (OutOfMemoryError ee) {
274            messager.error("main.out.of.memory");
275            failed = true;
276        } catch (ClientCodeException e) {
277            // simply rethrow these exceptions, to be caught and handled by JavadocTaskImpl
278            throw e;
279        } catch (Error ee) {
280            ee.printStackTrace(System.err);
281            messager.error("main.fatal.error");
282            failed = true;
283        } catch (Exception ee) {
284            ee.printStackTrace(System.err);
285            messager.error("main.fatal.exception");
286            failed = true;
287        } finally {
288            if (fileManager != null
289                    && fileManager instanceof BaseFileManager
290                    && ((BaseFileManager) fileManager).autoClose) {
291                try {
292                    fileManager.close();
293                } catch (IOException ignore) {}
294            }
295            boolean haveErrorWarnings = messager.nerrors() > 0 ||
296                    (rejectWarnings && messager.nwarnings() > 0);
297            if (failed && !haveErrorWarnings) {
298                // the doclet failed, but nothing reported, flag it!.
299                messager.error("main.unknown.error");
300            }
301            failed |= haveErrorWarnings;
302            messager.exitNotice();
303            messager.flush();
304        }
305        return !failed;
306    }
307
308    /**
309     * Main program - internal
310     */
311    private boolean parseAndExecute(List<String> argList,
312            Iterable<? extends JavaFileObject> fileObjects) throws IOException {
313        long tm = System.currentTimeMillis();
314
315        List<String> javaNames = new ArrayList<>();
316
317        compOpts = Options.instance(context);
318
319        // Make sure no obsolete source/target messages are reported
320        compOpts.put("-Xlint:-options", "-Xlint:-options");
321
322        doclet.init(locale, messager);
323        parseArgs(argList, javaNames);
324
325        if (fileManager instanceof BaseFileManager) {
326            ((BaseFileManager) fileManager).handleOptions(fileManagerOpts);
327        }
328
329        String platformString = compOpts.get("-release");
330
331        if (platformString != null) {
332            if (compOpts.isSet("-source")) {
333                usageError("main.release.bootclasspath.conflict", "-source");
334            }
335            if (fileManagerOpts.containsKey(BOOTCLASSPATH)) {
336                usageError("main.release.bootclasspath.conflict", BOOTCLASSPATH.getText());
337            }
338
339            PlatformDescription platformDescription =
340                    PlatformUtils.lookupPlatformDescription(platformString);
341
342            if (platformDescription == null) {
343                usageError("main.unsupported.release.version", platformString);
344            }
345
346            compOpts.put(SOURCE, platformDescription.getSourceVersion());
347
348            context.put(PlatformDescription.class, platformDescription);
349
350            Collection<Path> platformCP = platformDescription.getPlatformPath();
351
352            if (platformCP != null) {
353                if (fileManager instanceof StandardJavaFileManager) {
354                    StandardJavaFileManager sfm = (StandardJavaFileManager) fileManager;
355
356                    sfm.setLocationFromPaths(StandardLocation.PLATFORM_CLASS_PATH, platformCP);
357                } else {
358                    usageError("main.release.not.standard.file.manager", platformString);
359                }
360            }
361        }
362
363        compOpts.notifyListeners();
364
365        if (javaNames.isEmpty() && subPackages.isEmpty() && isEmpty(fileObjects)) {
366            usageError("main.No_packages_or_classes_specified");
367        }
368
369        JavadocTool comp = JavadocTool.make0(context);
370        if (comp == null) return false;
371
372        if (showAccess == null) {
373            setFilter(defaultModifier);
374        }
375
376        DocletEnvironment root = comp.getEnvironment(
377                encoding,
378                showAccess,
379                overviewpath,
380                javaNames,
381                fileObjects,
382                subPackages,
383                excludedPackages,
384                docClasses,
385                quiet);
386
387        // release resources
388        comp = null;
389
390        if (breakiterator || !locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
391            JavacTrees trees = JavacTrees.instance(context);
392            trees.setBreakIterator(BreakIterator.getSentenceInstance(locale));
393        }
394        // pass off control to the doclet
395        boolean ok = root != null;
396        if (ok) ok = doclet.run(root);
397
398        // We're done.
399        if (compOpts.get("-verbose") != null) {
400            tm = System.currentTimeMillis() - tm;
401            messager.notice("main.done_in", Long.toString(tm));
402        }
403
404        return ok;
405    }
406
407    Set<Doclet.Option> docletOptions = null;
408    int handleDocletOptions(int idx, List<String> args, boolean isToolOption) {
409        if (docletOptions == null) {
410            docletOptions = doclet.getSupportedOptions();
411        }
412        String arg = args.get(idx);
413
414        for (Doclet.Option opt : docletOptions) {
415            if (opt.matches(arg)) {
416                if (args.size() - idx < opt.getArgumentCount()) {
417                    usageError("main.requires_argument", arg);
418                }
419                opt.process(arg, args.listIterator(idx + 1));
420                idx += opt.getArgumentCount();
421                return idx;
422            }
423        }
424        // check if arg is accepted by the tool before emitting error
425        if (!isToolOption)
426            usageError("main.invalid_flag", arg);
427        return idx;
428    }
429
430    private Class<?> preProcess(JavaFileManager jfm, List<String> argv) {
431        // doclet specifying arguments
432        String userDocletPath = null;
433        String userDocletName = null;
434
435        // Step 1: loop through the args, set locale early on, if found.
436        for (int i = 0 ; i < argv.size() ; i++) {
437            String arg = argv.get(i);
438            if (arg.equals(ToolOption.LOCALE.opt)) {
439                oneArg(argv, i++);
440                String lname = argv.get(i);
441                locale = getLocale(lname);
442            } else if (arg.equals(ToolOption.DOCLET.opt)) {
443                oneArg(argv, i++);
444                if (userDocletName != null) {
445                    usageError("main.more_than_one_doclet_specified_0_and_1",
446                               userDocletName, argv.get(i));
447                }
448                if (docletName != null) {
449                    usageError("main.more_than_one_doclet_specified_0_and_1",
450                            docletName, argv.get(i));
451                }
452                userDocletName = argv.get(i);
453            } else if (arg.equals(ToolOption.DOCLETPATH.opt)) {
454                oneArg(argv, i++);
455                if (userDocletPath == null) {
456                    userDocletPath = argv.get(i);
457                } else {
458                    userDocletPath += File.pathSeparator + argv.get(i);
459                }
460            }
461        }
462        // Step 2: a doclet has already been provided,
463        // nothing more to do.
464        if (docletClass != null) {
465            return docletClass;
466        }
467        // Step 3: doclet name specified ? if so find a ClassLoader,
468        // and load it.
469        if (userDocletName != null) {
470            ClassLoader cl = classLoader;
471            if (cl == null) {
472                if (!fileManager.hasLocation(DOCLET_PATH)) {
473                    List<File> paths = new ArrayList<>();
474                    if (userDocletPath != null) {
475                        for (String pathname : userDocletPath.split(File.pathSeparator)) {
476                            paths.add(new File(pathname));
477                        }
478                    }
479                    try {
480                        ((StandardJavaFileManager)fileManager).setLocation(DOCLET_PATH, paths);
481                    } catch (IOException ioe) {
482                        panic("main.doclet_no_classloader_found", ioe);
483                        return null; // keep compiler happy
484                    }
485                }
486                cl = fileManager.getClassLoader(DOCLET_PATH);
487                if (cl == null) {
488                    // despite doclet specified on cmdline no classloader found!
489                    panic("main.doclet_no_classloader_found", userDocletName);
490                    return null; // keep compiler happy
491                }
492                try {
493                    return cl.loadClass(userDocletName);
494                } catch (ClassNotFoundException cnfe) {
495                    panic("main.doclet_class_not_found", userDocletName);
496                    return null; // keep compiler happy
497                }
498            }
499        }
500        // Step 4: we have a doclet, try loading it, otherwise
501        // return back the standard doclet
502        if (docletName != null) {
503            try {
504                return Class.forName(docletName, true, getClass().getClassLoader());
505            } catch (ClassNotFoundException cnfe) {
506                panic("main.doclet_class_not_found", userDocletName);
507                return null; // happy compiler, should not happen
508            }
509        } else {
510            return jdk.javadoc.internal.doclets.standard.Standard.class;
511        }
512    }
513
514    private void parseArgs(List<String> args, List<String> javaNames) {
515        for (int i = 0 ; i < args.size() ; i++) {
516            String arg = args.get(i);
517            ToolOption o = ToolOption.get(arg);
518            if (o != null) {
519                // handle a doclet argument that may be needed however
520                // don't increment the index, and allow the tool to consume args
521                handleDocletOptions(i, args, true);
522
523                if (o.hasArg) {
524                    oneArg(args, i++);
525                    o.process(this, args.get(i));
526                } else {
527                    setOption(arg);
528                    o.process(this);
529                }
530            } else if (arg.startsWith("-XD")) {
531                // hidden javac options
532                String s = arg.substring("-XD".length());
533                int eq = s.indexOf('=');
534                String key = (eq < 0) ? s : s.substring(0, eq);
535                String value = (eq < 0) ? s : s.substring(eq+1);
536                compOpts.put(key, value);
537            } else if (arg.startsWith("-")) {
538                i = handleDocletOptions(i, args, false);
539            } else {
540                javaNames.add(arg);
541            }
542        }
543    }
544
545    private <T> boolean isEmpty(Iterable<T> iter) {
546        return !iter.iterator().hasNext();
547    }
548
549    /**
550     * Set one arg option.
551     * Error and exit if one argument is not provided.
552     */
553    private void oneArg(List<String> args, int index) {
554        if ((index + 1) < args.size()) {
555            setOption(args.get(index), args.get(index+1));
556        } else {
557            usageError("main.requires_argument", args.get(index));
558        }
559    }
560
561    @Override
562    void usageError(String key, Object... args) {
563        error(key, args);
564        usage(true);
565    }
566
567    // a terminal call, will not return
568    void panic(String key, Object... args) {
569        error(key, args);
570        messager.exit();
571    }
572
573    void error(String key, Object... args) {
574        messager.error(key, args);
575    }
576
577    /**
578     * indicate an option with no arguments was given.
579     */
580    private void setOption(String opt) {
581        String[] option = { opt };
582        options.add(Arrays.asList(option));
583    }
584
585    /**
586     * indicate an option with one argument was given.
587     */
588    private void setOption(String opt, String argument) {
589        String[] option = { opt, argument };
590        options.add(Arrays.asList(option));
591    }
592
593    /**
594     * indicate an option with the specified list of arguments was given.
595     */
596    private void setOption(String opt, List<String> arguments) {
597        List<String> args = new ArrayList<>(arguments.size() + 1);
598        args.add(opt);
599        args.addAll(arguments);
600        options.add(args);
601    }
602
603    /**
604     * Get the locale if specified on the command line
605     * else return null and if locale option is not used
606     * then return default locale.
607     */
608    private Locale getLocale(String localeName) {
609        Locale userlocale = null;
610        if (localeName == null || localeName.isEmpty()) {
611            return Locale.getDefault();
612        }
613        int firstuscore = localeName.indexOf('_');
614        int seconduscore = -1;
615        String language = null;
616        String country = null;
617        String variant = null;
618        if (firstuscore == 2) {
619            language = localeName.substring(0, firstuscore);
620            seconduscore = localeName.indexOf('_', firstuscore + 1);
621            if (seconduscore > 0) {
622                if (seconduscore != firstuscore + 3
623                        || localeName.length() <= seconduscore + 1) {
624                    usageError("main.malformed_locale_name", localeName);
625                    return null;
626                }
627                country = localeName.substring(firstuscore + 1,
628                        seconduscore);
629                variant = localeName.substring(seconduscore + 1);
630            } else if (localeName.length() == firstuscore + 3) {
631                country = localeName.substring(firstuscore + 1);
632            } else {
633                usageError("main.malformed_locale_name", localeName);
634                return null;
635            }
636        } else if (firstuscore == -1 && localeName.length() == 2) {
637            language = localeName;
638        } else {
639            usageError("main.malformed_locale_name", localeName);
640            return null;
641        }
642        userlocale = searchLocale(language, country, variant);
643        if (userlocale == null) {
644            usageError("main.illegal_locale_name", localeName);
645            return null;
646        } else {
647            return userlocale;
648        }
649    }
650
651    /**
652     * Search the locale for specified language, specified country and
653     * specified variant.
654     */
655    private Locale searchLocale(String language, String country,
656                                String variant) {
657        for (Locale loc : Locale.getAvailableLocales()) {
658            if (loc.getLanguage().equals(language) &&
659                (country == null || loc.getCountry().equals(country)) &&
660                (variant == null || loc.getVariant().equals(variant))) {
661                return loc;
662            }
663        }
664        return null;
665    }
666}
667