1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.util; |
7 | |
8 | import java.io.ByteArrayOutputStream; |
9 | import java.io.DataInputStream; |
10 | import java.io.File; |
11 | import java.io.FileInputStream; |
12 | import java.io.IOException; |
13 | import java.io.InputStream; |
14 | import java.io.OutputStream; |
15 | import java.io.PrintStream; |
16 | import java.io.StringWriter; |
17 | import java.io.Writer; |
18 | import java.lang.reflect.Array; |
19 | import java.lang.reflect.Method; |
20 | import java.lang.reflect.Modifier; |
21 | import java.net.URI; |
22 | import java.security.SecureClassLoader; |
23 | import java.util.ArrayList; |
24 | import java.util.HashMap; |
25 | |
26 | import org.h2.api.ErrorCode; |
27 | import org.h2.engine.Constants; |
28 | import org.h2.engine.SysProperties; |
29 | import org.h2.message.DbException; |
30 | import org.h2.store.fs.FileUtils; |
31 | |
32 | import javax.tools.FileObject; |
33 | import javax.tools.ForwardingJavaFileManager; |
34 | import javax.tools.JavaCompiler; |
35 | import javax.tools.JavaFileManager; |
36 | import javax.tools.JavaFileObject; |
37 | import javax.tools.JavaFileObject.Kind; |
38 | import javax.tools.SimpleJavaFileObject; |
39 | import javax.tools.StandardJavaFileManager; |
40 | import javax.tools.ToolProvider; |
41 | |
42 | /** |
43 | * This class allows to convert source code to a class. It uses one class loader |
44 | * per class. |
45 | */ |
46 | public class SourceCompiler { |
47 | |
48 | /** |
49 | * The "com.sun.tools.javac.Main" (if available). |
50 | */ |
51 | static final JavaCompiler JAVA_COMPILER; |
52 | |
53 | private static final Class<?> JAVAC_SUN; |
54 | |
55 | private static final String COMPILE_DIR = |
56 | Utils.getProperty("java.io.tmpdir", "."); |
57 | |
58 | /** |
59 | * The class name to source code map. |
60 | */ |
61 | final HashMap<String, String> sources = New.hashMap(); |
62 | |
63 | /** |
64 | * The class name to byte code map. |
65 | */ |
66 | final HashMap<String, Class<?>> compiled = New.hashMap(); |
67 | |
68 | /** |
69 | * Whether to use the ToolProvider.getSystemJavaCompiler(). |
70 | */ |
71 | boolean useJavaSystemCompiler = SysProperties.JAVA_SYSTEM_COMPILER; |
72 | |
73 | static { |
74 | JavaCompiler c; |
75 | try { |
76 | c = ToolProvider.getSystemJavaCompiler(); |
77 | } catch (Exception e) { |
78 | // ignore |
79 | c = null; |
80 | } |
81 | JAVA_COMPILER = c; |
82 | Class<?> clazz; |
83 | try { |
84 | clazz = Class.forName("com.sun.tools.javac.Main"); |
85 | } catch (Exception e) { |
86 | clazz = null; |
87 | } |
88 | JAVAC_SUN = clazz; |
89 | } |
90 | |
91 | /** |
92 | * Set the source code for the specified class. |
93 | * This will reset all compiled classes. |
94 | * |
95 | * @param className the class name |
96 | * @param source the source code |
97 | */ |
98 | public void setSource(String className, String source) { |
99 | sources.put(className, source); |
100 | compiled.clear(); |
101 | } |
102 | |
103 | /** |
104 | * Enable or disable the usage of the Java system compiler. |
105 | * |
106 | * @param enabled true to enable |
107 | */ |
108 | public void setJavaSystemCompiler(boolean enabled) { |
109 | this.useJavaSystemCompiler = enabled; |
110 | } |
111 | |
112 | /** |
113 | * Get the class object for the given name. |
114 | * |
115 | * @param packageAndClassName the class name |
116 | * @return the class |
117 | */ |
118 | public Class<?> getClass(String packageAndClassName) |
119 | throws ClassNotFoundException { |
120 | |
121 | Class<?> compiledClass = compiled.get(packageAndClassName); |
122 | if (compiledClass != null) { |
123 | return compiledClass; |
124 | } |
125 | String source = sources.get(packageAndClassName); |
126 | if (isGroovySource(source)) { |
127 | Class<?> clazz = GroovyCompiler.parseClass(source, packageAndClassName); |
128 | compiled.put(packageAndClassName, clazz); |
129 | return clazz; |
130 | } |
131 | |
132 | ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) { |
133 | |
134 | @Override |
135 | public Class<?> findClass(String name) throws ClassNotFoundException { |
136 | Class<?> classInstance = compiled.get(name); |
137 | if (classInstance == null) { |
138 | String source = sources.get(name); |
139 | String packageName = null; |
140 | int idx = name.lastIndexOf('.'); |
141 | String className; |
142 | if (idx >= 0) { |
143 | packageName = name.substring(0, idx); |
144 | className = name.substring(idx + 1); |
145 | } else { |
146 | className = name; |
147 | } |
148 | String s = getCompleteSourceCode(packageName, className, source); |
149 | if (JAVA_COMPILER != null && useJavaSystemCompiler) { |
150 | classInstance = javaxToolsJavac(packageName, className, s); |
151 | } else { |
152 | byte[] data = javacCompile(packageName, className, s); |
153 | if (data == null) { |
154 | classInstance = findSystemClass(name); |
155 | } else { |
156 | classInstance = defineClass(name, data, 0, data.length); |
157 | } |
158 | } |
159 | compiled.put(name, classInstance); |
160 | } |
161 | return classInstance; |
162 | } |
163 | }; |
164 | return classLoader.loadClass(packageAndClassName); |
165 | } |
166 | |
167 | private static boolean isGroovySource(String source) { |
168 | return source.startsWith("//groovy") || source.startsWith("@groovy"); |
169 | } |
170 | |
171 | /** |
172 | * Get the first public static method of the given class. |
173 | * |
174 | * @param className the class name |
175 | * @return the method name |
176 | */ |
177 | public Method getMethod(String className) throws ClassNotFoundException { |
178 | Class<?> clazz = getClass(className); |
179 | Method[] methods = clazz.getDeclaredMethods(); |
180 | for (Method m : methods) { |
181 | int modifiers = m.getModifiers(); |
182 | if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers)) { |
183 | String name = m.getName(); |
184 | if (!name.startsWith("_") && !m.getName().equals("main")) { |
185 | return m; |
186 | } |
187 | } |
188 | } |
189 | return null; |
190 | } |
191 | |
192 | /** |
193 | * Compile the given class. This method tries to use the class |
194 | * "com.sun.tools.javac.Main" if available. If not, it tries to run "javac" |
195 | * in a separate process. |
196 | * |
197 | * @param packageName the package name |
198 | * @param className the class name |
199 | * @param source the source code |
200 | * @return the class file |
201 | */ |
202 | byte[] javacCompile(String packageName, String className, String source) { |
203 | File dir = new File(COMPILE_DIR); |
204 | if (packageName != null) { |
205 | dir = new File(dir, packageName.replace('.', '/')); |
206 | FileUtils.createDirectories(dir.getAbsolutePath()); |
207 | } |
208 | File javaFile = new File(dir, className + ".java"); |
209 | File classFile = new File(dir, className + ".class"); |
210 | try { |
211 | OutputStream f = FileUtils.newOutputStream(javaFile.getAbsolutePath(), false); |
212 | Writer out = IOUtils.getBufferedWriter(f); |
213 | classFile.delete(); |
214 | out.write(source); |
215 | out.close(); |
216 | if (JAVAC_SUN != null) { |
217 | javacSun(javaFile); |
218 | } else { |
219 | javacProcess(javaFile); |
220 | } |
221 | byte[] data = new byte[(int) classFile.length()]; |
222 | DataInputStream in = new DataInputStream(new FileInputStream(classFile)); |
223 | in.readFully(data); |
224 | in.close(); |
225 | return data; |
226 | } catch (Exception e) { |
227 | throw DbException.convert(e); |
228 | } finally { |
229 | javaFile.delete(); |
230 | classFile.delete(); |
231 | } |
232 | } |
233 | |
234 | /** |
235 | * Get the complete source code (including package name, imports, and so |
236 | * on). |
237 | * |
238 | * @param packageName the package name |
239 | * @param className the class name |
240 | * @param source the (possibly shortened) source code |
241 | * @return the full source code |
242 | */ |
243 | static String getCompleteSourceCode(String packageName, String className, |
244 | String source) { |
245 | if (source.startsWith("package ")) { |
246 | return source; |
247 | } |
248 | StringBuilder buff = new StringBuilder(); |
249 | if (packageName != null) { |
250 | buff.append("package ").append(packageName).append(";\n"); |
251 | } |
252 | int endImport = source.indexOf("@CODE"); |
253 | String importCode = |
254 | "import java.util.*;\n" + |
255 | "import java.math.*;\n" + |
256 | "import java.sql.*;\n"; |
257 | if (endImport >= 0) { |
258 | importCode = source.substring(0, endImport); |
259 | source = source.substring("@CODE".length() + endImport); |
260 | } |
261 | buff.append(importCode); |
262 | buff.append("public class ").append(className).append( |
263 | " {\n" + |
264 | " public static ").append(source).append("\n" + |
265 | "}\n"); |
266 | return buff.toString(); |
267 | } |
268 | |
269 | /** |
270 | * Compile using the standard java compiler. |
271 | * |
272 | * @param packageName the package name |
273 | * @param className the class name |
274 | * @param source the source code |
275 | * @return the class |
276 | */ |
277 | Class<?> javaxToolsJavac(String packageName, String className, String source) { |
278 | String fullClassName = packageName + "." + className; |
279 | StringWriter writer = new StringWriter(); |
280 | JavaFileManager fileManager = new |
281 | ClassFileManager(JAVA_COMPILER |
282 | .getStandardFileManager(null, null, null)); |
283 | ArrayList<JavaFileObject> compilationUnits = new ArrayList<JavaFileObject>(); |
284 | compilationUnits.add(new StringJavaFileObject(fullClassName, source)); |
285 | JAVA_COMPILER.getTask(writer, fileManager, null, null, |
286 | null, compilationUnits).call(); |
287 | String err = writer.toString(); |
288 | throwSyntaxError(err); |
289 | try { |
290 | return fileManager.getClassLoader(null).loadClass(fullClassName); |
291 | } catch (ClassNotFoundException e) { |
292 | throw DbException.convert(e); |
293 | } |
294 | } |
295 | |
296 | private static void javacProcess(File javaFile) { |
297 | exec("javac", |
298 | "-sourcepath", COMPILE_DIR, |
299 | "-d", COMPILE_DIR, |
300 | "-encoding", "UTF-8", |
301 | javaFile.getAbsolutePath()); |
302 | } |
303 | |
304 | private static int exec(String... args) { |
305 | ByteArrayOutputStream buff = new ByteArrayOutputStream(); |
306 | try { |
307 | ProcessBuilder builder = new ProcessBuilder(); |
308 | // The javac executable allows some of it's flags |
309 | // to be smuggled in via environment variables. |
310 | // But if it sees those flags, it will write out a message |
311 | // to stderr, which messes up our parsing of the output. |
312 | builder.environment().remove("JAVA_TOOL_OPTIONS"); |
313 | builder.command(args); |
314 | |
315 | Process p = builder.start(); |
316 | copyInThread(p.getInputStream(), buff); |
317 | copyInThread(p.getErrorStream(), buff); |
318 | p.waitFor(); |
319 | String err = new String(buff.toByteArray(), Constants.UTF8); |
320 | throwSyntaxError(err); |
321 | return p.exitValue(); |
322 | } catch (Exception e) { |
323 | throw DbException.convert(e); |
324 | } |
325 | } |
326 | |
327 | private static void copyInThread(final InputStream in, final OutputStream out) { |
328 | new Task() { |
329 | @Override |
330 | public void call() throws IOException { |
331 | IOUtils.copy(in, out); |
332 | } |
333 | }.execute(); |
334 | } |
335 | |
336 | private static void javacSun(File javaFile) { |
337 | PrintStream old = System.err; |
338 | ByteArrayOutputStream buff = new ByteArrayOutputStream(); |
339 | PrintStream temp = new PrintStream(buff); |
340 | try { |
341 | System.setErr(temp); |
342 | Method compile; |
343 | compile = JAVAC_SUN.getMethod("compile", String[].class); |
344 | Object javac = JAVAC_SUN.newInstance(); |
345 | compile.invoke(javac, (Object) new String[] { |
346 | "-sourcepath", COMPILE_DIR, |
347 | "-d", COMPILE_DIR, |
348 | "-encoding", "UTF-8", |
349 | javaFile.getAbsolutePath() }); |
350 | String err = new String(buff.toByteArray(), Constants.UTF8); |
351 | throwSyntaxError(err); |
352 | } catch (Exception e) { |
353 | throw DbException.convert(e); |
354 | } finally { |
355 | System.setErr(old); |
356 | } |
357 | } |
358 | |
359 | private static void throwSyntaxError(String err) { |
360 | if (err.startsWith("Note:")) { |
361 | // unchecked or unsafe operations - just a warning |
362 | } else if (err.length() > 0) { |
363 | err = StringUtils.replaceAll(err, COMPILE_DIR, ""); |
364 | throw DbException.get(ErrorCode.SYNTAX_ERROR_1, err); |
365 | } |
366 | } |
367 | |
368 | |
369 | /** |
370 | * Access the Groovy compiler using reflection, so that we do not gain a |
371 | * compile-time dependency unnecessarily. |
372 | */ |
373 | private static final class GroovyCompiler { |
374 | |
375 | private static final Object LOADER; |
376 | private static final Throwable INIT_FAIL_EXCEPTION; |
377 | |
378 | static { |
379 | Object loader = null; |
380 | Throwable initFailException = null; |
381 | try { |
382 | // Create an instance of ImportCustomizer |
383 | Class<?> importCustomizerClass = Class.forName( |
384 | "org.codehaus.groovy.control.customizers.ImportCustomizer"); |
385 | Object importCustomizer = Utils.newInstance( |
386 | "org.codehaus.groovy.control.customizers.ImportCustomizer"); |
387 | // Call the method ImportCustomizer.addImports(String[]) |
388 | String[] importsArray = new String[] { |
389 | "java.sql.Connection", |
390 | "java.sql.Types", |
391 | "java.sql.ResultSet", |
392 | "groovy.sql.Sql", |
393 | "org.h2.tools.SimpleResultSet" |
394 | }; |
395 | Utils.callMethod(importCustomizer, "addImports", new Object[] { importsArray }); |
396 | |
397 | // Call the method |
398 | // CompilerConfiguration.addCompilationCustomizers( |
399 | // ImportCustomizer...) |
400 | Object importCustomizerArray = Array.newInstance(importCustomizerClass, 1); |
401 | Array.set(importCustomizerArray, 0, importCustomizer); |
402 | Object configuration = Utils.newInstance( |
403 | "org.codehaus.groovy.control.CompilerConfiguration"); |
404 | Utils.callMethod(configuration, |
405 | "addCompilationCustomizers", new Object[] { importCustomizerArray }); |
406 | |
407 | ClassLoader parent = GroovyCompiler.class.getClassLoader(); |
408 | loader = Utils.newInstance( |
409 | "groovy.lang.GroovyClassLoader", parent, configuration); |
410 | } catch (Exception ex) { |
411 | initFailException = ex; |
412 | } |
413 | LOADER = loader; |
414 | INIT_FAIL_EXCEPTION = initFailException; |
415 | } |
416 | |
417 | public static Class<?> parseClass(String source, |
418 | String packageAndClassName) { |
419 | if (LOADER == null) { |
420 | throw new RuntimeException( |
421 | "Compile fail: no Groovy jar in the classpath", INIT_FAIL_EXCEPTION); |
422 | } |
423 | try { |
424 | Object codeSource = Utils.newInstance("groovy.lang.GroovyCodeSource", |
425 | source, packageAndClassName + ".groovy", "UTF-8"); |
426 | Utils.callMethod(codeSource, "setCachable", false); |
427 | Class<?> clazz = (Class<?>) Utils.callMethod( |
428 | LOADER, "parseClass", codeSource); |
429 | return clazz; |
430 | } catch (Exception e) { |
431 | throw new RuntimeException(e); |
432 | } |
433 | } |
434 | } |
435 | |
436 | /** |
437 | * An in-memory java source file object. |
438 | */ |
439 | static class StringJavaFileObject extends SimpleJavaFileObject { |
440 | |
441 | private final String sourceCode; |
442 | |
443 | public StringJavaFileObject(String className, String sourceCode) { |
444 | super(URI.create("string:///" + className.replace('.', '/') |
445 | + Kind.SOURCE.extension), Kind.SOURCE); |
446 | this.sourceCode = sourceCode; |
447 | } |
448 | |
449 | @Override |
450 | public CharSequence getCharContent(boolean ignoreEncodingErrors) { |
451 | return sourceCode; |
452 | } |
453 | |
454 | } |
455 | |
456 | /** |
457 | * An in-memory java class object. |
458 | */ |
459 | static class JavaClassObject extends SimpleJavaFileObject { |
460 | |
461 | private final ByteArrayOutputStream out = new ByteArrayOutputStream(); |
462 | |
463 | public JavaClassObject(String name, Kind kind) { |
464 | super(URI.create("string:///" + name.replace('.', '/') |
465 | + kind.extension), kind); |
466 | } |
467 | |
468 | public byte[] getBytes() { |
469 | return out.toByteArray(); |
470 | } |
471 | |
472 | @Override |
473 | public OutputStream openOutputStream() throws IOException { |
474 | return out; |
475 | } |
476 | } |
477 | |
478 | /** |
479 | * An in-memory class file manager. |
480 | */ |
481 | static class ClassFileManager extends |
482 | ForwardingJavaFileManager<StandardJavaFileManager> { |
483 | |
484 | /** |
485 | * The class (only one class is kept). |
486 | */ |
487 | JavaClassObject classObject; |
488 | |
489 | public ClassFileManager(StandardJavaFileManager standardManager) { |
490 | super(standardManager); |
491 | } |
492 | |
493 | @Override |
494 | public ClassLoader getClassLoader(Location location) { |
495 | return new SecureClassLoader() { |
496 | @Override |
497 | protected Class<?> findClass(String name) |
498 | throws ClassNotFoundException { |
499 | byte[] bytes = classObject.getBytes(); |
500 | return super.defineClass(name, bytes, 0, |
501 | bytes.length); |
502 | } |
503 | }; |
504 | } |
505 | |
506 | @Override |
507 | public JavaFileObject getJavaFileForOutput(Location location, |
508 | String className, Kind kind, FileObject sibling) throws IOException { |
509 | classObject = new JavaClassObject(className, kind); |
510 | return classObject; |
511 | } |
512 | } |
513 | |
514 | } |