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