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