/* * $Id: LuaScriptEngine.java 53 2012-01-05 16:58:58Z andre@naef.com $ * See LICENSE.txt for license terms. */ package com.naef.jnlua.script; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; 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.ScriptEngineFactory; import javax.script.ScriptException; import com.naef.jnlua.LuaException; import com.naef.jnlua.LuaState; /** * Lua script engine implementation conforming to JSR 223: Scripting for the * Java Platform. */ class LuaScriptEngine extends AbstractScriptEngine implements Compilable, Invocable { // -- Static private static final String READER = "reader"; private static final String WRITER = "writer"; private static final String ERROR_WRITER = "errorWriter"; private static final Pattern LUA_ERROR_MESSAGE = Pattern .compile("^(.+):(\\d+):"); // -- State private LuaScriptEngineFactory factory; private LuaState luaState; // -- Construction /** * Creates a new instance. */ LuaScriptEngine(LuaScriptEngineFactory factory) { super(); this.factory = factory; luaState = new LuaState(); // Configuration context.setBindings(createBindings(), ScriptContext.ENGINE_SCOPE); luaState.openLibs(); } // -- ScriptEngine methods @Override public Bindings createBindings() { return new LuaBindings(this); } @Override public Object eval(String script, ScriptContext context) throws ScriptException { synchronized (luaState) { loadChunk(script, context); return callChunk(context); } } @Override public Object eval(Reader reader, ScriptContext context) throws ScriptException { synchronized (luaState) { loadChunk(reader, context); return callChunk(context); } } @Override public ScriptEngineFactory getFactory() { return factory; } // -- Compilable method @Override public CompiledScript compile(String script) throws ScriptException { ByteArrayOutputStream out = new ByteArrayOutputStream(); synchronized (luaState) { loadChunk(script, null); try { dumpChunk(out); } finally { luaState.pop(1); } } return new CompiledLuaScript(this, out.toByteArray()); } @Override public CompiledScript compile(Reader script) throws ScriptException { ByteArrayOutputStream out = new ByteArrayOutputStream(); synchronized (luaState) { loadChunk(script, null); try { dumpChunk(out); } finally { luaState.pop(1); } } return new CompiledLuaScript(this, out.toByteArray()); } // -- Invocable methods @Override public <T> T getInterface(Class<T> clasz) { synchronized (luaState) { getLuaState().rawGet(LuaState.REGISTRYINDEX, LuaState.RIDX_GLOBALS); try { return luaState.getProxy(-1, clasz); } finally { luaState.pop(1); } } } @Override public <T> T getInterface(Object thiz, Class<T> clasz) { synchronized (luaState) { luaState.pushJavaObject(thiz); try { if (!luaState.isTable(-1)) { throw new IllegalArgumentException("object is not a table"); } return luaState.getProxy(-1, clasz); } finally { luaState.pop(1); } } } @Override public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { synchronized (luaState) { luaState.getGlobal(name); if (!luaState.isFunction(-1)) { luaState.pop(1); throw new NoSuchMethodException(String.format( "function '%s' is undefined", name)); } for (int i = 0; i < args.length; i++) { luaState.pushJavaObject(args[i]); } luaState.call(args.length, 1); try { return luaState.toJavaObject(-1, Object.class); } finally { luaState.pop(1); } } } @Override public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException { synchronized (luaState) { luaState.pushJavaObject(thiz); try { if (!luaState.isTable(-1)) { throw new IllegalArgumentException("object is not a table"); } luaState.getField(-1, name); if (!luaState.isFunction(-1)) { luaState.pop(1); throw new NoSuchMethodException(String.format( "method '%s' is undefined", name)); } luaState.pushValue(-2); for (int i = 0; i < args.length; i++) { luaState.pushJavaObject(args[i]); } luaState.call(args.length + 1, 1); try { return luaState.toJavaObject(-1, Object.class); } finally { luaState.pop(1); } } finally { luaState.pop(1); } } } // -- Package private methods /** * Returns the Lua state. */ LuaState getLuaState() { return luaState; } /** * Loads a chunk from a string. */ void loadChunk(String string, ScriptContext scriptContext) throws ScriptException { try { luaState.load(string, getChunkName(scriptContext)); } catch (LuaException e) { throw getScriptException(e); } } /** * Loads a chunk from a reader. */ void loadChunk(Reader reader, ScriptContext scriptContext) throws ScriptException { loadChunk(new ReaderInputStream(reader), scriptContext, "t"); } /** * Loads a chunk from an input stream. */ void loadChunk(InputStream inputStream, ScriptContext scriptContext, String mode) throws ScriptException { try { luaState.load(inputStream, getChunkName(scriptContext), mode); } catch (LuaException e) { throw getScriptException(e); } catch (IOException e) { throw new ScriptException(e); } } /** * Calls a loaded chunk. */ Object callChunk(ScriptContext context) throws ScriptException { try { // Apply context Object[] argv; if (context != null) { // Global bindings Bindings bindings; bindings = context.getBindings(ScriptContext.GLOBAL_SCOPE); if (bindings != null) { applyBindings(bindings); } // Engine bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE); if (bindings != null) { if (bindings instanceof LuaBindings && ((LuaBindings) bindings).getScriptEngine() == this) { // No need to apply our own live bindings } else { applyBindings(bindings); } } // Readers and writers put(READER, context.getReader()); put(WRITER, context.getWriter()); put(ERROR_WRITER, context.getErrorWriter()); // Arguments argv = (Object[]) context.getAttribute(ARGV); } else { argv = null; } // Push arguments int argCount = argv != null ? argv.length : 0; for (int i = 0; i < argCount; i++) { luaState.pushJavaObject(argv[i]); } // Call luaState.call(argCount, 1); // Return try { return luaState.toJavaObject(1, Object.class); } finally { luaState.pop(1); } } catch (LuaException e) { throw getScriptException(e); } } /** * Dumps a loaded chunk into an output stream. The chunk is left on the * stack. */ void dumpChunk(OutputStream out) throws ScriptException { try { luaState.dump(out); } catch (LuaException e) { throw new ScriptException(e); } catch (IOException e) { throw new ScriptException(e); } } // -- Private methods /** * Sets a single binding in a Lua state. */ private void applyBindings(Bindings bindings) { for (Map.Entry<String, Object> binding : bindings.entrySet()) { luaState.pushJavaObject(binding.getValue()); String variableName = binding.getKey(); int lastDotIndex = variableName.lastIndexOf('.'); if (lastDotIndex >= 0) { variableName = variableName.substring(lastDotIndex + 1); } luaState.setGlobal(variableName); } } /** * Returns the Lua chunk name from a script context. */ private String getChunkName(ScriptContext context) { if (context != null) { Object fileName = context.getAttribute(FILENAME); if (fileName != null) { return "@" + fileName.toString(); } } return "=null"; } /** * Returns a script exception for a Lua exception. */ private ScriptException getScriptException(LuaException e) { Matcher matcher = LUA_ERROR_MESSAGE.matcher(e.getMessage()); if (matcher.find()) { String fileName = matcher.group(1); int lineNumber = Integer.parseInt(matcher.group(2)); return new ScriptException(e.getMessage(), fileName, lineNumber); } else { return new ScriptException(e); } } // -- Private classes /** * Provides an UTF-8 input stream based on a reader. */ private static class ReaderInputStream extends InputStream { // -- Static private static final Charset UTF8 = Charset.forName("UTF-8"); // -- State private Reader reader; private CharsetEncoder encoder; private boolean flushed; private CharBuffer charBuffer = CharBuffer.allocate(1024); private ByteBuffer byteBuffer = ByteBuffer.allocate(1024); /** * Creates a new instance. */ public ReaderInputStream(Reader reader) { this.reader = reader; encoder = UTF8.newEncoder(); charBuffer.limit(0); byteBuffer.limit(0); } @Override public int read() throws IOException { if (!byteBuffer.hasRemaining()) { if (!charBuffer.hasRemaining()) { charBuffer.clear(); reader.read(charBuffer); charBuffer.flip(); } byteBuffer.clear(); if (charBuffer.hasRemaining()) { if (encoder.encode(charBuffer, byteBuffer, false).isError()) { throw new IOException("Encoding error"); } } else { if (!flushed) { if (encoder.encode(charBuffer, byteBuffer, true) .isError()) { throw new IOException("Encoding error"); } if (encoder.flush(byteBuffer).isError()) { throw new IOException("Encoding error"); } flushed = true; } } byteBuffer.flip(); if (!byteBuffer.hasRemaining()) { return -1; } } return byteBuffer.get(); } } }