DocletInvoker.java revision 4104:4012b3f11f0d
1/*
2 * Copyright (c) 1998, 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 com.sun.tools.javadoc.main;
27
28import java.io.File;
29import java.io.IOException;
30import java.lang.reflect.InvocationTargetException;
31import java.lang.reflect.Method;
32import java.lang.reflect.Modifier;
33import java.net.MalformedURLException;
34import java.net.URL;
35import java.net.URLClassLoader;
36import java.nio.file.Path;
37import java.nio.file.Paths;
38import java.util.ArrayList;
39import java.util.regex.Pattern;
40
41import javax.tools.DocumentationTool;
42import javax.tools.JavaFileManager;
43
44import com.sun.javadoc.*;
45import com.sun.tools.javac.util.ClientCodeException;
46import com.sun.tools.javac.util.List;
47
48/**
49 * Class creates, controls and invokes doclets.
50 *
51 *  <p><b>This is NOT part of any supported API.
52 *  If you write code that depends on this, you do so at your own risk.
53 *  This code and its internal interfaces are subject to change or
54 *  deletion without notice.</b>
55 *
56 * @author Neal Gafter (rewrite)
57 */
58@Deprecated
59public class DocletInvoker {
60
61    private final Class<?> docletClass;
62
63    private final String docletClassName;
64
65    private final ClassLoader appClassLoader;
66
67    private final Messager messager;
68
69    /**
70     * In API mode, exceptions thrown while calling the doclet are
71     * propagated using ClientCodeException.
72     */
73    private final boolean apiMode;
74
75    /**
76     * Whether javadoc internal API should be exported to doclets
77     * and (indirectly) to taglets
78     */
79    private final boolean exportInternalAPI;
80
81    private static class DocletInvokeException extends Exception {
82        private static final long serialVersionUID = 0;
83    }
84
85    private String appendPath(String path1, String path2) {
86        if (path1 == null || path1.length() == 0) {
87            return path2 == null ? "." : path2;
88        } else if (path2 == null || path2.length() == 0) {
89            return path1;
90        } else {
91            return path1  + File.pathSeparator + path2;
92        }
93    }
94
95    public DocletInvoker(Messager messager, Class<?> docletClass, boolean apiMode, boolean exportInternalAPI) {
96        this.messager = messager;
97        this.docletClass = docletClass;
98        docletClassName = docletClass.getName();
99        appClassLoader = null;
100        this.apiMode = apiMode;
101        this.exportInternalAPI = exportInternalAPI; // for backdoor use by standard doclet for taglets
102
103        // this may not be soon enough if the class has already been loaded
104        if (exportInternalAPI) {
105            exportInternalAPI(docletClass.getClassLoader());
106        }
107    }
108
109    public DocletInvoker(Messager messager, JavaFileManager fileManager,
110                         String docletClassName, String docletPath,
111                         ClassLoader docletParentClassLoader,
112                         boolean apiMode,
113                         boolean exportInternalAPI) {
114        this.messager = messager;
115        this.docletClassName = docletClassName;
116        this.apiMode = apiMode;
117        this.exportInternalAPI = exportInternalAPI; // for backdoor use by standard doclet for taglets
118
119        if (fileManager != null && fileManager.hasLocation(DocumentationTool.Location.DOCLET_PATH)) {
120            appClassLoader = fileManager.getClassLoader(DocumentationTool.Location.DOCLET_PATH);
121        } else {
122            // construct class loader
123            String cpString = null;   // make sure env.class.path defaults to dot
124
125            // do prepends to get correct ordering
126            cpString = appendPath(System.getProperty("env.class.path"), cpString);
127            cpString = appendPath(System.getProperty("java.class.path"), cpString);
128            cpString = appendPath(docletPath, cpString);
129            URL[] urls = pathToURLs(cpString);
130            if (docletParentClassLoader == null)
131                appClassLoader = new URLClassLoader(urls, getDelegationClassLoader(docletClassName));
132            else
133                appClassLoader = new URLClassLoader(urls, docletParentClassLoader);
134        }
135
136        if (exportInternalAPI) {
137            exportInternalAPI(appClassLoader);
138        }
139
140        // attempt to find doclet
141        Class<?> dc = null;
142        try {
143            dc = appClassLoader.loadClass(docletClassName);
144        } catch (ClassNotFoundException exc) {
145            messager.error(Messager.NOPOS, "main.doclet_class_not_found", docletClassName);
146            messager.exit();
147        }
148        docletClass = dc;
149    }
150
151    /*
152     * Returns the delegation class loader to use when creating
153     * appClassLoader (used to load the doclet).  The context class
154     * loader is the best choice, but legacy behavior was to use the
155     * default delegation class loader (aka system class loader).
156     *
157     * Here we favor using the context class loader.  To ensure
158     * compatibility with existing apps, we revert to legacy
159     * behavior if either or both of the following conditions hold:
160     *
161     * 1) the doclet is loadable from the system class loader but not
162     *    from the context class loader,
163     *
164     * 2) this.getClass() is loadable from the system class loader but not
165     *    from the context class loader.
166     */
167    private ClassLoader getDelegationClassLoader(String docletClassName) {
168        ClassLoader ctxCL = Thread.currentThread().getContextClassLoader();
169        ClassLoader sysCL = ClassLoader.getSystemClassLoader();
170        if (sysCL == null)
171            return ctxCL;
172        if (ctxCL == null)
173            return sysCL;
174
175        // Condition 1.
176        try {
177            sysCL.loadClass(docletClassName);
178            try {
179                ctxCL.loadClass(docletClassName);
180            } catch (ClassNotFoundException e) {
181                return sysCL;
182            }
183        } catch (ClassNotFoundException e) {
184        }
185
186        // Condition 2.
187        try {
188            if (getClass() == sysCL.loadClass(getClass().getName())) {
189                try {
190                    if (getClass() != ctxCL.loadClass(getClass().getName()))
191                        return sysCL;
192                } catch (ClassNotFoundException e) {
193                    return sysCL;
194                }
195            }
196        } catch (ClassNotFoundException e) {
197        }
198
199        return ctxCL;
200    }
201
202    /**
203     * Generate documentation here.  Return true on success.
204     */
205    public boolean start(RootDoc root) {
206        Object retVal;
207        String methodName = "start";
208        Class<?>[] paramTypes = { RootDoc.class };
209        Object[] params = { root };
210        try {
211            retVal = invoke(methodName, null, paramTypes, params);
212        } catch (DocletInvokeException exc) {
213            return false;
214        }
215        if (retVal instanceof Boolean) {
216            return ((Boolean)retVal);
217        } else {
218            messager.error(Messager.NOPOS, "main.must_return_boolean",
219                           docletClassName, methodName);
220            return false;
221        }
222    }
223
224    /**
225     * Check for doclet added options here. Zero return means
226     * option not known.  Positive value indicates number of
227     * arguments to option.  Negative value means error occurred.
228     */
229    public int optionLength(String option) {
230        Object retVal;
231        String methodName = "optionLength";
232        Class<?>[] paramTypes = { String.class };
233        Object[] params = { option };
234        try {
235            retVal = invoke(methodName, 0, paramTypes, params);
236        } catch (DocletInvokeException exc) {
237            return -1;
238        }
239        if (retVal instanceof Integer) {
240            return ((Integer)retVal);
241        } else {
242            messager.error(Messager.NOPOS, "main.must_return_int",
243                           docletClassName, methodName);
244            return -1;
245        }
246    }
247
248    /**
249     * Let doclet check that all options are OK. Returning true means
250     * options are OK.  If method does not exist, assume true.
251     */
252    public boolean validOptions(List<String[]> optlist) {
253        Object retVal;
254        String options[][] = optlist.toArray(new String[optlist.length()][]);
255        String methodName = "validOptions";
256        DocErrorReporter reporter = messager;
257        Class<?>[] paramTypes = { String[][].class, DocErrorReporter.class };
258        Object[] params = { options, reporter };
259        try {
260            retVal = invoke(methodName, Boolean.TRUE, paramTypes, params);
261        } catch (DocletInvokeException exc) {
262            return false;
263        }
264        if (retVal instanceof Boolean) {
265            return ((Boolean)retVal);
266        } else {
267            messager.error(Messager.NOPOS, "main.must_return_boolean",
268                           docletClassName, methodName);
269            return false;
270        }
271    }
272
273    /**
274     * Return the language version supported by this doclet.
275     * If the method does not exist in the doclet, assume version 1.1.
276     */
277    public LanguageVersion languageVersion() {
278        try {
279            Object retVal;
280            String methodName = "languageVersion";
281            Class<?>[] paramTypes = new Class<?>[0];
282            Object[] params = new Object[0];
283            try {
284                retVal = invoke(methodName, LanguageVersion.JAVA_1_1, paramTypes, params);
285            } catch (DocletInvokeException exc) {
286                return LanguageVersion.JAVA_1_1;
287            }
288            if (retVal instanceof LanguageVersion) {
289                return (LanguageVersion)retVal;
290            } else {
291                messager.error(Messager.NOPOS, "main.must_return_languageversion",
292                               docletClassName, methodName);
293                return LanguageVersion.JAVA_1_1;
294            }
295        } catch (NoClassDefFoundError ex) { // for boostrapping, no Enum class.
296            return null;
297        }
298    }
299
300    /**
301     * Utility method for calling doclet functionality
302     */
303    private Object invoke(String methodName, Object returnValueIfNonExistent,
304                          Class<?>[] paramTypes, Object[] params)
305        throws DocletInvokeException {
306            Method meth;
307            try {
308                meth = docletClass.getMethod(methodName, paramTypes);
309            } catch (NoSuchMethodException exc) {
310                if (returnValueIfNonExistent == null) {
311                    messager.error(Messager.NOPOS, "main.doclet_method_not_found",
312                                   docletClassName, methodName);
313                    throw new DocletInvokeException();
314                } else {
315                    return returnValueIfNonExistent;
316                }
317            } catch (SecurityException exc) {
318                messager.error(Messager.NOPOS, "main.doclet_method_not_accessible",
319                               docletClassName, methodName);
320                throw new DocletInvokeException();
321            }
322            if (!Modifier.isStatic(meth.getModifiers())) {
323                messager.error(Messager.NOPOS, "main.doclet_method_must_be_static",
324                               docletClassName, methodName);
325                throw new DocletInvokeException();
326            }
327            ClassLoader savedCCL =
328                Thread.currentThread().getContextClassLoader();
329            try {
330                if (appClassLoader != null) // will be null if doclet class provided via API
331                    Thread.currentThread().setContextClassLoader(appClassLoader);
332                return meth.invoke(null , params);
333            } catch (IllegalArgumentException | NullPointerException exc) {
334                messager.error(Messager.NOPOS, "main.internal_error_exception_thrown",
335                               docletClassName, methodName, exc.toString());
336                throw new DocletInvokeException();
337            } catch (IllegalAccessException exc) {
338                messager.error(Messager.NOPOS, "main.doclet_method_not_accessible",
339                               docletClassName, methodName);
340                throw new DocletInvokeException();
341            }
342            catch (InvocationTargetException exc) {
343                Throwable err = exc.getTargetException();
344                if (apiMode)
345                    throw new ClientCodeException(err);
346                if (err instanceof java.lang.OutOfMemoryError) {
347                    messager.error(Messager.NOPOS, "main.out.of.memory");
348                } else {
349                    messager.error(Messager.NOPOS, "main.exception_thrown",
350                               docletClassName, methodName, exc.toString());
351                    exc.getTargetException().printStackTrace(System.err);
352                }
353                throw new DocletInvokeException();
354            } finally {
355                Thread.currentThread().setContextClassLoader(savedCCL);
356            }
357    }
358
359    /**
360     * Export javadoc internal API to the unnamed module for a classloader.
361     * This is to support continued use of existing non-standard doclets that
362     * use the internal toolkit API and related classes.
363     * @param cl the classloader
364     */
365    private void exportInternalAPI(ClassLoader cl) {
366        String[] packages = {
367            "com.sun.tools.doclets",
368            "com.sun.tools.doclets.standard",
369            "com.sun.tools.doclets.internal.toolkit",
370            "com.sun.tools.doclets.internal.toolkit.taglets",
371            "com.sun.tools.doclets.internal.toolkit.builders",
372            "com.sun.tools.doclets.internal.toolkit.util",
373            "com.sun.tools.doclets.internal.toolkit.util.links",
374            "com.sun.tools.doclets.formats.html",
375            "com.sun.tools.doclets.formats.html.markup"
376        };
377
378        try {
379            Method getModuleMethod = Class.class.getDeclaredMethod("getModule");
380            Object thisModule = getModuleMethod.invoke(getClass());
381
382            Class<?> moduleClass = Class.forName("java.lang.Module");
383            Method addExportsMethod = moduleClass.getDeclaredMethod("addExports", String.class, moduleClass);
384
385            Method getUnnamedModuleMethod = ClassLoader.class.getDeclaredMethod("getUnnamedModule");
386            Object target = getUnnamedModuleMethod.invoke(cl);
387
388            for (String pack : packages) {
389                addExportsMethod.invoke(thisModule, pack, target);
390            }
391        } catch (Exception e) {
392            // do nothing
393        }
394    }
395
396    /**
397     * Utility method for converting a search path string to an array of directory and JAR file
398     * URLs.
399     *
400     * Note that this method is called by the DocletInvoker.
401     *
402     * @param path the search path string
403     * @return the resulting array of directory and JAR file URLs
404     */
405    private static URL[] pathToURLs(String path) {
406        java.util.List<URL> urls = new ArrayList<>();
407        for (String s: path.split(Pattern.quote(File.pathSeparator))) {
408            if (!s.isEmpty()) {
409                URL url = fileToURL(Paths.get(s));
410                if (url != null) {
411                    urls.add(url);
412                }
413            }
414        }
415        return urls.toArray(new URL[urls.size()]);
416    }
417
418    /**
419     * Returns the directory or JAR file URL corresponding to the specified local file name.
420     *
421     * @param file the Path object
422     * @return the resulting directory or JAR file URL, or null if unknown
423     */
424    private static URL fileToURL(Path file) {
425        Path p;
426        try {
427            p = file.toRealPath();
428        } catch (IOException e) {
429            p = file.toAbsolutePath();
430        }
431        try {
432            return p.normalize().toUri().toURL();
433        } catch (MalformedURLException e) {
434            return null;
435        }
436    }
437}
438