/*
* Copyright 2006 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met: Redistributions of source code
* must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution. Neither the name of the Sun Microsystems nor the names of
* is contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.l2jserver.script.jython;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import org.python.compiler.Module;
import org.python.core.BytecodeLoader;
import org.python.core.Py;
import org.python.core.PyCode;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.core.PySystemState;
import org.python.core.__builtin__;
import org.python.core.parser;
/**
* @author A. Sundararajan
*/
public class JythonScriptEngine extends AbstractScriptEngine implements Compilable, Invocable
{
// my factory, may be null
private ScriptEngineFactory factory;
// my scope -- associated with the default context
private PyObject myScope;
public static final String JYTHON_COMPILE_MODE = "com.sun.script.jython.comp.mode";
public static final String JYTHON_ENGINE_INSTANCE = "com.l2jserver.script.jython.engine.instance";
private static ThreadLocal<PySystemState> systemState;
static
{
PySystemState.initialize();
systemState = new ThreadLocal<PySystemState>();
}
public JythonScriptEngine()
{
myScope = newScope(context);
}
// my implementation for CompiledScript
private static class JythonCompiledScript extends CompiledScript implements Serializable
{
private static final long serialVersionUID = 1L;
// my compiled code
private transient PyCode code;
private transient JythonScriptEngine _engine;
private final String _name;
private final byte _data[];
private final String _filename;
JythonCompiledScript(JythonScriptEngine engine, String name, byte data[], String filename)
{
_engine = engine;
_name = name;
_data = data;
_filename = filename;
code = BytecodeLoader.makeCode(name, data, filename);
}
@Override
public ScriptEngine getEngine()
{
return _engine;
}
@Override
public Object eval(ScriptContext ctx) throws ScriptException
{
if (code == null)
{
code = BytecodeLoader.makeCode(_name, _data, _filename);
}
if (_engine == null)
{
_engine = (JythonScriptEngine) ctx.getAttribute(JYTHON_ENGINE_INSTANCE);
if (_engine == null)
{
throw new IllegalStateException("Cannot eval an deserialized ScriptContext without passing an jython engine instance through the ScriptContext");
}
}
return _engine.evalCode(code, ctx);
}
}
// Compilable methods
@Override
public CompiledScript compile(String script) throws ScriptException
{
return compileSerializableScript(script, context);
}
@Override
public CompiledScript compile(Reader reader) throws ScriptException
{
return compile(readFully(reader));
}
// Invocable methods
@Override
public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException
{
return invokeImpl(null, name, args);
}
@Override
public Object invokeMethod(Object obj, String name, Object... args) throws ScriptException, NoSuchMethodException
{
if (obj == null)
{
throw new IllegalArgumentException("script object is null");
}
else
{
return invokeImpl(obj, name, args);
}
}
private Object invokeImpl(Object obj, String name, Object... args) throws ScriptException, NoSuchMethodException
{
if (name == null)
{
throw new NullPointerException("method name is null");
}
setSystemState();
PyObject thiz;
if (obj instanceof PyObject)
{
thiz = (PyObject) obj;
}
else if (obj == null)
{
thiz = myScope;
}
else
{
thiz = java2py(obj);
}
PyObject func = thiz.__findattr__(name);
if (((func == null) || !func.isCallable()) && (thiz == myScope))
{
systemState.get();
// lookup in built-in functions. This way
// user can call invoke built-in functions.
PyObject builtins = PySystemState.builtins;
func = builtins.__finditem__(name);
}
if ((func == null) || !func.isCallable())
{
throw new NoSuchMethodException(name);
}
PyObject res = func.__call__(wrapArguments(args));
return py2java(res);
}
@Override
public <T> T getInterface(Object obj, Class<T> clazz)
{
if (obj == null)
{
throw new IllegalArgumentException("script object is null");
}
else
{
return makeInterface(obj, clazz);
}
}
@Override
public <T> T getInterface(Class<T> clazz)
{
return makeInterface(null, clazz);
}
@SuppressWarnings("unchecked")
private <T> T makeInterface(Object obj, Class<T> clazz)
{
if ((clazz == null) || !clazz.isInterface())
{
throw new IllegalArgumentException("interface Class expected");
}
final Object thiz = obj;
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]
{
clazz
}, new InvocationHandler()
{
@Override
public Object invoke(Object proxy, Method m, Object args[]) throws Throwable
{
Object res = invokeImpl(thiz, m.getName(), args);
return py2java(java2py(res), m.getReturnType());
}
});
}
// ScriptEngine methods
@Override
public Object eval(String str, ScriptContext ctx) throws ScriptException
{
PyCode code = compileScript(str, ctx);
return evalCode(code, ctx);
}
@Override
public Object eval(Reader reader, ScriptContext ctx) throws ScriptException
{
return eval(readFully(reader), ctx);
}
@Override
public ScriptEngineFactory getFactory()
{
synchronized (this)
{
if (factory == null)
{
factory = new JythonScriptEngineFactory();
}
}
return factory;
}
@Override
public Bindings createBindings()
{
return new SimpleBindings();
}
@Override
public void setContext(ScriptContext ctx)
{
super.setContext(ctx);
// update myScope to keep it in-sync
myScope = newScope(context);
}
// package-private methods
void setFactory(ScriptEngineFactory factory)
{
this.factory = factory;
}
static PyObject java2py(Object javaObj)
{
return Py.java2py(javaObj);
}
static Object py2java(PyObject pyObj, Class<?> type)
{
return (pyObj == null) ? null : pyObj.__tojava__(type);
}
static Object py2java(PyObject pyObj)
{
return py2java(pyObj, Object.class);
}
static PyObject[] wrapArguments(Object args[])
{
if (args == null)
{
return new PyObject[0];
}
PyObject res[] = new PyObject[args.length];
for (int i = 0; i < args.length; i++)
{
res[i] = java2py(args[i]);
}
return res;
}
// internals only below this point
private PyObject getJythonScope(ScriptContext ctx)
{
if (ctx == context)
{
return myScope;
}
else
{
return newScope(ctx);
}
}
private PyObject newScope(ScriptContext ctx)
{
return new JythonScope(this, ctx);
}
private void setSystemState()
{
/*
* From my reading of Jython source, it appears that PySystemState is set on per-thread basis. So, I maintain it in a thread local and set it. Besides, this also helps in setting correct class loader -- which is thread context class loader.
*/
if (systemState.get() == null)
{
// we entering into this thread for the first time.
PySystemState newState = new PySystemState();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
newState.setClassLoader(cl);
systemState.set(newState);
Py.setSystemState(newState);
}
}
private PyCode compileScript(String script, ScriptContext ctx) throws ScriptException
{
try
{
setSystemState();
String fileName = (String) ctx.getAttribute(ScriptEngine.FILENAME);
if (fileName == null)
{
fileName = "<unknown>";
}
/*
* Jython parser seems to have 3 input modes (called compile "kind") These are "single", "eval" and "exec". I don't clearly understand the difference. But, with "eval" and "exec" certain features are not working. For eg. with "eval" assignments are not working. I've used "exec". But,
* that is customizable by special attribute.
*/
String mode = (String) ctx.getAttribute(JYTHON_COMPILE_MODE);
if (mode == null)
{
mode = "exec";
}
return __builtin__.compile(script, fileName, mode);
}
catch (Exception exp)
{
throw new ScriptException(exp);
}
}
private JythonCompiledScript compileSerializableScript(String script, ScriptContext ctx) throws ScriptException
{
try
{
setSystemState();
String fileName = (String) ctx.getAttribute(ScriptEngine.FILENAME);
if (fileName == null)
{
fileName = "<unknown>";
}
/*
* Jython parser seems to have 3 input modes (called compile "kind") These are "single", "eval" and "exec". I don't clearly understand the difference. But, with "eval" and "exec" certain features are not working. For eg. with "eval" assignments are not working. I've used "exec". But,
* that is customizable by special attribute.
*/
String mode = (String) ctx.getAttribute(JYTHON_COMPILE_MODE);
if (mode == null)
{
mode = "exec";
}
org.python.parser.ast.modType node = parser.parse(new ByteArrayInputStream(PyString.to_bytes(script + "\n\n")), mode, fileName, Py.getCompilerFlags());
ByteArrayOutputStream ostream = new ByteArrayOutputStream();
String name = "org.python.pycode.serializable._pyx" + System.currentTimeMillis();
Module.compile(node, ostream, name, fileName, true, false, false, Py.getCompilerFlags());
return new JythonCompiledScript(this, name, ostream.toByteArray(), fileName);
}
catch (Exception exp)
{
throw new ScriptException(exp);
}
}
private Object evalCode(PyCode code, ScriptContext ctx) throws ScriptException
{
try
{
PyObject res;
setSystemState();
PyObject scope = getJythonScope(ctx);
res = Py.runCode(code, scope, scope);
return res.__tojava__(Object.class);
}
catch (Exception exp)
{
throw new ScriptException(exp);
}
}
private String readFully(Reader reader) throws ScriptException
{
char arr[] = new char[8 * 1024]; // 8K at a time
StringBuilder buf = new StringBuilder();
int numChars;
try
{
while ((numChars = reader.read(arr, 0, arr.length)) > 0)
{
buf.append(arr, 0, numChars);
}
}
catch (IOException exp)
{
throw new ScriptException(exp);
}
return buf.toString();
}
}