/*
* Copyright 2013 Cloudera Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kitesdk.morphline.scriptengine.java;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Creates and compiles the given Java code block, wrapped into a Java method with the given return
* type and parameter types, along with a Java class definition that contains the given import
* statements.
* <p>
* Compilation is done in main memory, i.e. without writing to the filesystem.
* <p>
* The result is an object that can be executed (and reused) any number of times. This is a high
* performance implementation, using an optimized variant of https://scripting.dev.java.net/" (JSR
* 223 Java Scripting). Calling {@link #evaluate(Object...)} just means calling
* {@link Method#invoke(Object, Object...)} and as such has the same minimal runtime cost, i.e.
* O(100M calls/sec/core).
*
* Instances of this class are thread-safe if the user provided script statements are thread-safe.
*/
public class ScriptEvaluator<T> {
private final FastJavaScriptEngine.JavaCompiledScript compiledScript;
private final String javaCodeBlock;
private final String parseLocation;
private static final AtomicLong nextClassNum = new AtomicLong();
private static final String METHOD_NAME = "eval";
private static final Logger LOG = LoggerFactory.getLogger(ScriptEvaluator.class);
public ScriptEvaluator(String javaImports, String javaCodeBlock, Class<T> returnType,
String[] parameterNames, Class[] parameterTypes,
String parseLocation) throws ScriptException {
this(javaImports, javaCodeBlock, returnType, parameterNames, parameterTypes, new Class[0], parseLocation);
}
public ScriptEvaluator(String javaImports, String javaCodeBlock, Class<T> returnType,
String[] parameterNames, Class[] parameterTypes, Class[] throwTypes,
String parseLocation) throws ScriptException {
if (parameterNames.length != parameterTypes.length) {
throw new IllegalArgumentException(
"Lengths of parameterNames (" + parameterNames.length
+ ") and parameterTypes (" + parameterTypes.length
+ ") do not match");
}
this.javaCodeBlock = javaCodeBlock;
this.parseLocation = parseLocation;
String myPackageName = getClass().getName();
myPackageName = myPackageName.substring(0, myPackageName.lastIndexOf('.'));
String className = "MyJavaClass" + nextClassNum.incrementAndGet();
String returnTypeName = (returnType == Void.class ? "void" : returnType.getCanonicalName());
String script =
"package " + myPackageName + ".scripts;"
+ "\n"
+ javaImports
+ "\n"
+ "\n public final class " + className + " {"
+ "\n public static " + returnTypeName + " " + METHOD_NAME + "(";
for (int i = 0; i < parameterNames.length; i++) {
if (i > 0) {
script += ", ";
}
script += parameterTypes[i].getCanonicalName() + " " + parameterNames[i];
}
script += ") ";
if (throwTypes.length > 0) {
script += "throws ";
for (int i = 0; i < throwTypes.length; i++) {
if (i > 0) {
script += ", ";
}
script += throwTypes[i].getCanonicalName();
}
script += " ";
}
script += "{ " + javaCodeBlock + " }";
script += "\n }";
LOG.trace("Compiling script: \n{}", script);
FastJavaScriptEngine engine = new FastJavaScriptEngine();
StringWriter errorWriter = new StringWriter();
engine.getContext().setErrorWriter(errorWriter);
engine.getContext().setAttribute(ScriptEngine.FILENAME, className + ".java", ScriptContext.ENGINE_SCOPE);
ClassLoader[] loaders = getClassLoaders();
engine.getContext().setAttribute("parentLoader", loaders[0], ScriptContext.ENGINE_SCOPE);
try {
compiledScript = (FastJavaScriptEngine.JavaCompiledScript) engine.compile(script, METHOD_NAME, parameterTypes);
} catch (ScriptException e) {
String errorMsg = errorWriter.toString();
if (errorMsg.length() > 0) {
errorMsg = ": " + errorMsg;
}
throwScriptCompilationException(parseLocation, e.getMessage() + errorMsg, null);
throw null; // keep compiler happy
}
engine.getContext().setErrorWriter(new PrintWriter(System.err, true)); // reset
}
@SuppressWarnings("unchecked")
public T evaluate(Object... params) throws ScriptException {
// TODO: consider restricting permissions/sandboxing; also see http://worldwizards.blogspot.com/2009/08/java-scripting-api-sandbox.html
try {
return (T) compiledScript.eval(params);
} catch (ScriptException e) {
throwScriptExecutionException(parseLocation + " near: '" + javaCodeBlock + "'", params, e);
}
return null; // keep compiler happy
}
private ClassLoader[] getClassLoaders() {
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
ClassLoader myLoader = getClass().getClassLoader();
if (contextLoader == null) {
return new ClassLoader[] { myLoader };
} else if (contextLoader == myLoader || myLoader == null) {
return new ClassLoader[] { contextLoader };
} else {
return new ClassLoader[] { contextLoader, myLoader };
}
}
private static void throwScriptCompilationException(String parseLocation, String msg, Throwable t)
throws ScriptException {
if (t == null) {
throw new ScriptException("Cannot compile script: " + parseLocation + " caused by " + msg);
} else {
ScriptException se = new ScriptException("Cannot compile script: " + parseLocation + " caused by " + msg);
se.initCause(t);
throw se;
}
}
private static void throwScriptExecutionException(String parseLocation, Object[] params, Throwable e)
throws ScriptException {
ScriptException se = new ScriptException("Cannot execute script: " + parseLocation + " for params " + Arrays.asList(params).toString());
se.initCause(e);
throw se;
}
}