Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.h2.util.SourceCompiler Maven / Gradle / Ivy
/*
* Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.util;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.SecureClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import org.h2.api.ErrorCode;
import org.h2.engine.SysProperties;
import org.h2.message.DbException;
import org.h2.store.fs.FileUtils;
/**
* This class allows to convert source code to a class. It uses one class loader
* per class.
*/
public class SourceCompiler {
/**
* The "com.sun.tools.javac.Main" (if available).
*/
static final JavaCompiler JAVA_COMPILER;
private static final Class> JAVAC_SUN;
private static final String COMPILE_DIR =
Utils.getProperty("java.io.tmpdir", ".");
/**
* The class name to source code map.
*/
final HashMap sources = new HashMap<>();
/**
* The class name to byte code map.
*/
final HashMap> compiled = new HashMap<>();
/**
* The class name to compiled scripts map.
*/
final Map compiledScripts = new HashMap<>();
/**
* Whether to use the ToolProvider.getSystemJavaCompiler().
*/
boolean useJavaSystemCompiler = SysProperties.JAVA_SYSTEM_COMPILER;
static {
JavaCompiler c;
try {
c = ToolProvider.getSystemJavaCompiler();
} catch (Exception e) {
// ignore
c = null;
}
JAVA_COMPILER = c;
Class> clazz;
try {
clazz = Class.forName("com.sun.tools.javac.Main");
} catch (Exception e) {
clazz = null;
}
JAVAC_SUN = clazz;
}
/**
* Set the source code for the specified class.
* This will reset all compiled classes.
*
* @param className the class name
* @param source the source code
*/
public void setSource(String className, String source) {
sources.put(className, source);
compiled.clear();
}
/**
* Enable or disable the usage of the Java system compiler.
*
* @param enabled true to enable
*/
public void setJavaSystemCompiler(boolean enabled) {
this.useJavaSystemCompiler = enabled;
}
/**
* Get the class object for the given name.
*
* @param packageAndClassName the class name
* @return the class
*/
public Class> getClass(String packageAndClassName)
throws ClassNotFoundException {
Class> compiledClass = compiled.get(packageAndClassName);
if (compiledClass != null) {
return compiledClass;
}
String source = sources.get(packageAndClassName);
if (isGroovySource(source)) {
Class> clazz = GroovyCompiler.parseClass(source, packageAndClassName);
compiled.put(packageAndClassName, clazz);
return clazz;
}
ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) {
@Override
public Class> findClass(String name) throws ClassNotFoundException {
Class> classInstance = compiled.get(name);
if (classInstance == null) {
String source = sources.get(name);
String packageName = null;
int idx = name.lastIndexOf('.');
String className;
if (idx >= 0) {
packageName = name.substring(0, idx);
className = name.substring(idx + 1);
} else {
className = name;
}
String s = getCompleteSourceCode(packageName, className, source);
if (JAVA_COMPILER != null && useJavaSystemCompiler) {
classInstance = javaxToolsJavac(packageName, className, s);
} else {
byte[] data = javacCompile(packageName, className, s);
if (data == null) {
classInstance = findSystemClass(name);
} else {
classInstance = defineClass(name, data, 0, data.length);
}
}
compiled.put(name, classInstance);
}
return classInstance;
}
};
return classLoader.loadClass(packageAndClassName);
}
private static boolean isGroovySource(String source) {
return source.startsWith("//groovy") || source.startsWith("@groovy");
}
private static boolean isJavascriptSource(String source) {
return source.startsWith("//javascript");
}
private static boolean isRubySource(String source) {
return source.startsWith("#ruby");
}
/**
* Whether the passed source can be compiled using {@link javax.script.ScriptEngineManager}.
*
* @param source the source to test.
* @return true
if {@link #getCompiledScript(String)} can be called.
*/
public static boolean isJavaxScriptSource(String source) {
return isJavascriptSource(source) || isRubySource(source);
}
/**
* Get the compiled script.
*
* @param packageAndClassName the package and class name
* @return the compiled script
*/
public CompiledScript getCompiledScript(String packageAndClassName) throws ScriptException {
CompiledScript compiledScript = compiledScripts.get(packageAndClassName);
if (compiledScript == null) {
String source = sources.get(packageAndClassName);
final String lang;
if (isJavascriptSource(source)) {
lang = "javascript";
} else if (isRubySource(source)) {
lang = "ruby";
} else {
throw new IllegalStateException("Unknown language for " + source);
}
final Compilable jsEngine = (Compilable) new ScriptEngineManager().getEngineByName(lang);
compiledScript = jsEngine.compile(source);
compiledScripts.put(packageAndClassName, compiledScript);
}
return compiledScript;
}
/**
* Get the first public static method of the given class.
*
* @param className the class name
* @return the method name
*/
public Method getMethod(String className) throws ClassNotFoundException {
Class> clazz = getClass(className);
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
int modifiers = m.getModifiers();
if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers)) {
String name = m.getName();
if (!name.startsWith("_") && !m.getName().equals("main")) {
return m;
}
}
}
return null;
}
/**
* Compile the given class. This method tries to use the class
* "com.sun.tools.javac.Main" if available. If not, it tries to run "javac"
* in a separate process.
*
* @param packageName the package name
* @param className the class name
* @param source the source code
* @return the class file
*/
byte[] javacCompile(String packageName, String className, String source) {
File dir = new File(COMPILE_DIR);
if (packageName != null) {
dir = new File(dir, packageName.replace('.', '/'));
FileUtils.createDirectories(dir.getAbsolutePath());
}
File javaFile = new File(dir, className + ".java");
File classFile = new File(dir, className + ".class");
try {
OutputStream f = FileUtils.newOutputStream(javaFile.getAbsolutePath(), false);
Writer out = IOUtils.getBufferedWriter(f);
classFile.delete();
out.write(source);
out.close();
if (JAVAC_SUN != null) {
javacSun(javaFile);
} else {
javacProcess(javaFile);
}
byte[] data = new byte[(int) classFile.length()];
DataInputStream in = new DataInputStream(new FileInputStream(classFile));
in.readFully(data);
in.close();
return data;
} catch (Exception e) {
throw DbException.convert(e);
} finally {
javaFile.delete();
classFile.delete();
}
}
/**
* Get the complete source code (including package name, imports, and so
* on).
*
* @param packageName the package name
* @param className the class name
* @param source the (possibly shortened) source code
* @return the full source code
*/
static String getCompleteSourceCode(String packageName, String className,
String source) {
if (source.startsWith("package ")) {
return source;
}
StringBuilder buff = new StringBuilder();
if (packageName != null) {
buff.append("package ").append(packageName).append(";\n");
}
int endImport = source.indexOf("@CODE");
String importCode =
"import java.util.*;\n" +
"import java.math.*;\n" +
"import java.sql.*;\n";
if (endImport >= 0) {
importCode = source.substring(0, endImport);
source = source.substring("@CODE".length() + endImport);
}
buff.append(importCode);
buff.append("public class ").append(className).append(
" {\n" +
" public static ").append(source).append("\n" +
"}\n");
return buff.toString();
}
/**
* Compile using the standard java compiler.
*
* @param packageName the package name
* @param className the class name
* @param source the source code
* @return the class
*/
Class> javaxToolsJavac(String packageName, String className, String source) {
String fullClassName = packageName + "." + className;
StringWriter writer = new StringWriter();
try (JavaFileManager fileManager = new
ClassFileManager(JAVA_COMPILER
.getStandardFileManager(null, null, null))) {
ArrayList compilationUnits = new ArrayList<>();
compilationUnits.add(new StringJavaFileObject(fullClassName, source));
// cannot concurrently compile
final boolean ok;
synchronized (JAVA_COMPILER) {
ok = JAVA_COMPILER.getTask(writer, fileManager, null, null,
null, compilationUnits).call();
}
String output = writer.toString();
handleSyntaxError(output, (ok? 0: 1));
return fileManager.getClassLoader(null).loadClass(fullClassName);
} catch (ClassNotFoundException | IOException e) {
throw DbException.convert(e);
}
}
private static void javacProcess(File javaFile) {
exec("javac",
"-sourcepath", COMPILE_DIR,
"-d", COMPILE_DIR,
"-encoding", "UTF-8",
javaFile.getAbsolutePath());
}
private static int exec(String... args) {
ByteArrayOutputStream buff = new ByteArrayOutputStream();
try {
ProcessBuilder builder = new ProcessBuilder();
// The javac executable allows some of it's flags
// to be smuggled in via environment variables.
// But if it sees those flags, it will write out a message
// to stderr, which messes up our parsing of the output.
builder.environment().remove("JAVA_TOOL_OPTIONS");
builder.command(args);
Process p = builder.start();
copyInThread(p.getInputStream(), buff);
copyInThread(p.getErrorStream(), buff);
p.waitFor();
String output = new String(buff.toByteArray(), StandardCharsets.UTF_8);
handleSyntaxError(output, p.exitValue());
return p.exitValue();
} catch (Exception e) {
throw DbException.convert(e);
}
}
private static void copyInThread(final InputStream in, final OutputStream out) {
new Task() {
@Override
public void call() throws IOException {
IOUtils.copy(in, out);
}
}.execute();
}
private static synchronized void javacSun(File javaFile) {
PrintStream old = System.err;
ByteArrayOutputStream buff = new ByteArrayOutputStream();
PrintStream temp = new PrintStream(buff);
try {
System.setErr(temp);
Method compile;
compile = JAVAC_SUN.getMethod("compile", String[].class);
Object javac = JAVAC_SUN.getDeclaredConstructor().newInstance();
// Bugfix: Here we should check exit status value instead of parsing javac output text.
// Because of the output text is different in different locale environment.
// @since 2018-07-20 little-pan
final Integer status = (Integer)compile.invoke(javac, (Object) new String[] {
"-sourcepath", COMPILE_DIR,
// "-Xlint:unchecked",
"-d", COMPILE_DIR,
"-encoding", "UTF-8",
javaFile.getAbsolutePath() });
String output = new String(buff.toByteArray(), StandardCharsets.UTF_8);
handleSyntaxError(output, status);
} catch (Exception e) {
throw DbException.convert(e);
} finally {
System.setErr(old);
}
}
private static void handleSyntaxError(String output, int exitStatus) {
if(0 == exitStatus){
return;
}
boolean syntaxError = false;
final BufferedReader reader = new BufferedReader(new StringReader(output));
try {
for (String line; (line = reader.readLine()) != null;) {
if (line.endsWith("warning") || line.endsWith("warnings")) {
// ignore summary line
} else if (line.startsWith("Note:")
|| line.startsWith("warning:")) {
// just a warning (e.g. unchecked or unsafe operations)
} else {
syntaxError = true;
break;
}
}
} catch (IOException ignored) {
// exception ignored
}
if (syntaxError) {
output = StringUtils.replaceAll(output, COMPILE_DIR, "");
throw DbException.get(ErrorCode.SYNTAX_ERROR_1, output);
}
}
/**
* Access the Groovy compiler using reflection, so that we do not gain a
* compile-time dependency unnecessarily.
*/
private static final class GroovyCompiler {
private static final Object LOADER;
private static final Throwable INIT_FAIL_EXCEPTION;
static {
Object loader = null;
Throwable initFailException = null;
try {
// Create an instance of ImportCustomizer
Class> importCustomizerClass = Class.forName(
"org.codehaus.groovy.control.customizers.ImportCustomizer");
Object importCustomizer = Utils.newInstance(
"org.codehaus.groovy.control.customizers.ImportCustomizer");
// Call the method ImportCustomizer.addImports(String[])
String[] importsArray = {
"java.sql.Connection",
"java.sql.Types",
"java.sql.ResultSet",
"groovy.sql.Sql",
"org.h2.tools.SimpleResultSet"
};
Utils.callMethod(importCustomizer, "addImports", new Object[] { importsArray });
// Call the method
// CompilerConfiguration.addCompilationCustomizers(
// ImportCustomizer...)
Object importCustomizerArray = Array.newInstance(importCustomizerClass, 1);
Array.set(importCustomizerArray, 0, importCustomizer);
Object configuration = Utils.newInstance(
"org.codehaus.groovy.control.CompilerConfiguration");
Utils.callMethod(configuration,
"addCompilationCustomizers", importCustomizerArray);
ClassLoader parent = GroovyCompiler.class.getClassLoader();
loader = Utils.newInstance(
"groovy.lang.GroovyClassLoader", parent, configuration);
} catch (Exception ex) {
initFailException = ex;
}
LOADER = loader;
INIT_FAIL_EXCEPTION = initFailException;
}
public static Class> parseClass(String source,
String packageAndClassName) {
if (LOADER == null) {
throw new RuntimeException(
"Compile fail: no Groovy jar in the classpath", INIT_FAIL_EXCEPTION);
}
try {
Object codeSource = Utils.newInstance("groovy.lang.GroovyCodeSource",
source, packageAndClassName + ".groovy", "UTF-8");
Utils.callMethod(codeSource, "setCachable", false);
return (Class>) Utils.callMethod(
LOADER, "parseClass", codeSource);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* An in-memory java source file object.
*/
static class StringJavaFileObject extends SimpleJavaFileObject {
private final String sourceCode;
public StringJavaFileObject(String className, String sourceCode) {
super(URI.create("string:///" + className.replace('.', '/')
+ Kind.SOURCE.extension), Kind.SOURCE);
this.sourceCode = sourceCode;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return sourceCode;
}
}
/**
* An in-memory java class object.
*/
static class JavaClassObject extends SimpleJavaFileObject {
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
public JavaClassObject(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/')
+ kind.extension), kind);
}
public byte[] getBytes() {
return out.toByteArray();
}
@Override
public OutputStream openOutputStream() throws IOException {
return out;
}
}
/**
* An in-memory class file manager.
*/
static class ClassFileManager extends
ForwardingJavaFileManager {
/**
* The class (only one class is kept).
*/
JavaClassObject classObject;
public ClassFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
}
@Override
public ClassLoader getClassLoader(Location location) {
return new SecureClassLoader() {
@Override
protected Class> findClass(String name)
throws ClassNotFoundException {
byte[] bytes = classObject.getBytes();
return super.defineClass(name, bytes, 0,
bytes.length);
}
};
}
@Override
public JavaFileObject getJavaFileForOutput(Location location,
String className, Kind kind, FileObject sibling) throws IOException {
classObject = new JavaClassObject(className, kind);
return classObject;
}
}
}