/* * 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 org.codehaus.groovy.jsr223; import groovy.lang.Binding; import groovy.lang.Closure; import groovy.lang.DelegatingMetaClass; import groovy.lang.GroovyClassLoader; import groovy.lang.MetaClass; import groovy.lang.MissingMethodException; import groovy.lang.MissingPropertyException; import groovy.lang.Script; import groovy.lang.Tuple; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.syntax.SyntaxException; import org.codehaus.groovy.runtime.InvokerHelper; import org.codehaus.groovy.runtime.MetaClassHelper; import org.codehaus.groovy.runtime.MethodClosure; 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 javax.script.SimpleBindings; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.io.Writer; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collections; import java.util.HashMap; import java.util.Map; /* * @author Mike Grogan * @author A. Sundararajan * @author Jim White * @author Guillaume Laforge */ public class GroovyScriptEngineImpl extends AbstractScriptEngine implements Compilable, Invocable { private static boolean debug = false; // script-string-to-generated Class map private Map<String, Class> classMap; // global closures map - this is used to simulate a single // global functions namespace private Map<String, Closure> globalClosures; // class loader for Groovy generated classes private GroovyClassLoader loader; // lazily initialized factory private volatile GroovyScriptEngineFactory factory; // counter used to generate unique global Script class names private static int counter; static { counter = 0; } public GroovyScriptEngineImpl() { classMap = Collections.synchronizedMap(new HashMap<String, Class>()); globalClosures = Collections.synchronizedMap(new HashMap<String, Closure>()); loader = new GroovyClassLoader(getParentLoader(), new CompilerConfiguration()); } public Object eval(Reader reader, ScriptContext ctx) throws ScriptException { return eval(readFully(reader), ctx); } public Object eval(String script, ScriptContext ctx) throws ScriptException { try { Class clazz = getScriptClass(script); if (clazz == null) throw new ScriptException("Script class is null"); return eval(clazz, ctx); } catch (SyntaxException e) { throw new ScriptException(e.getMessage(), e.getSourceLocator(), e.getLine()); } catch (Exception e) { if (debug) e.printStackTrace(); throw new ScriptException(e); } } public Bindings createBindings() { return new SimpleBindings(); } public ScriptEngineFactory getFactory() { if (factory == null) { synchronized (this) { if (factory == null) { factory = new GroovyScriptEngineFactory(); } } } return factory; } // javax.script.Compilable methods public CompiledScript compile(String scriptSource) throws ScriptException { try { return new GroovyCompiledScript(this, getScriptClass(scriptSource)); } catch (SyntaxException e) { throw new ScriptException(e.getMessage(), e.getSourceLocator(), e.getLine()); } catch (IOException e) { throw new ScriptException(e); } catch (CompilationFailedException ee) { throw new ScriptException(ee); } } public CompiledScript compile(Reader reader) throws ScriptException { return compile(readFully(reader)); } // javax.script.Invokable methods. public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { return invokeImpl(null, name, args); } public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException { if (thiz == null) { throw new IllegalArgumentException("script object is null"); } return invokeImpl(thiz, name, args); } public <T> T getInterface(Class<T> clazz) { return makeInterface(null, clazz); } public <T> T getInterface(Object thiz, Class<T> clazz) { if (thiz == null) { throw new IllegalArgumentException("script object is null"); } return makeInterface(thiz, clazz); } // package-privates Object eval(Class scriptClass, final ScriptContext ctx) throws ScriptException { // Bindings so script has access to this environment. // Only initialize once. if (null == ctx.getAttribute("context", ScriptContext.ENGINE_SCOPE)) { // add context to bindings ctx.setAttribute("context", ctx, ScriptContext.ENGINE_SCOPE); // direct output to ctx.getWriter // If we're wrapping with a PrintWriter here, // enable autoFlush because otherwise it might not get done! final Writer writer = ctx.getWriter(); ctx.setAttribute("out", (writer instanceof PrintWriter) ? writer : new PrintWriter(writer, true), ScriptContext.ENGINE_SCOPE); // Not going to do this after all (at least for now). // Scripts can use context.{reader, writer, errorWriter}. // That is a modern version of System.{in, out, err} or Console.{reader, writer}(). // // // New I/O names consistent with ScriptContext and java.io.Console. // // ctx.setAttribute("writer", writer, ScriptContext.ENGINE_SCOPE); // // // Direct errors to ctx.getErrorWriter // final Writer errorWriter = ctx.getErrorWriter(); // ctx.setAttribute("errorWriter", (errorWriter instanceof PrintWriter) ? // errorWriter : // new PrintWriter(errorWriter), // ScriptContext.ENGINE_SCOPE); // // // Get input from ctx.getReader // // We don't wrap with BufferedReader here because we expect that if // // the host wants that they do it. Either way Groovy scripts will // // always have readLine because the GDK supplies it for Reader. // ctx.setAttribute("reader", ctx.getReader(), ScriptContext.ENGINE_SCOPE); } // Fix for GROOVY-3669: Can't use several times the same JSR-223 ScriptContext for differents groovy script if (ctx.getWriter() != null) { ctx.setAttribute("out", new PrintWriter(ctx.getWriter(), true), ScriptContext.ENGINE_SCOPE); } /* * We use the following Binding instance so that global variable lookup * will be done in the current ScriptContext instance. */ Binding binding = new Binding(ctx.getBindings(ScriptContext.ENGINE_SCOPE)) { @Override public Object getVariable(String name) { synchronized (ctx) { int scope = ctx.getAttributesScope(name); if (scope != -1) { return ctx.getAttribute(name, scope); } } throw new MissingPropertyException(name, getClass()); } @Override public void setVariable(String name, Object value) { synchronized (ctx) { int scope = ctx.getAttributesScope(name); if (scope == -1) { scope = ScriptContext.ENGINE_SCOPE; } ctx.setAttribute(name, value, scope); } } }; try { // if this class is not an instance of Script, it's a full-blown class // then simply return that class if (!Script.class.isAssignableFrom(scriptClass)) { return scriptClass; } else { // it's a script Script scriptObject = (Script) scriptClass.newInstance(); scriptObject.setBinding(binding); // create a Map of MethodClosures from this new script object Method[] methods = scriptClass.getMethods(); Map<String, Closure> closures = new HashMap<String, Closure>(); for (Method m : methods) { String name = m.getName(); closures.put(name, new MethodClosure(scriptObject, name)); } // save all current closures into global closures map globalClosures.putAll(closures); MetaClass oldMetaClass = scriptObject.getMetaClass(); /* * We override the MetaClass of this script object so that we can * forward calls to global closures (of previous or future "eval" calls) * This gives the illusion of working on the same "global" scope. */ scriptObject.setMetaClass(new DelegatingMetaClass(oldMetaClass) { @Override public Object invokeMethod(Object object, String name, Object args) { if (args == null) { return invokeMethod(object, name, MetaClassHelper.EMPTY_ARRAY); } if (args instanceof Tuple) { return invokeMethod(object, name, ((Tuple) args).toArray()); } if (args instanceof Object[]) { return invokeMethod(object, name, (Object[]) args); } else { return invokeMethod(object, name, new Object[]{args}); } } @Override public Object invokeMethod(Object object, String name, Object[] args) { try { return super.invokeMethod(object, name, args); } catch (MissingMethodException mme) { return callGlobal(name, args, ctx); } } @Override public Object invokeStaticMethod(Object object, String name, Object[] args) { try { return super.invokeStaticMethod(object, name, args); } catch (MissingMethodException mme) { return callGlobal(name, args, ctx); } } }); return scriptObject.run(); } } catch (Exception e) { throw new ScriptException(e); } finally { // Fix for GROOVY-3669: Can't use several times the same JSR-223 ScriptContext for different groovy script // Groovy's scripting engine implementation adds those two variables in the binding // but should clean up afterwards ctx.removeAttribute("context", ScriptContext.ENGINE_SCOPE); ctx.removeAttribute("out", ScriptContext.ENGINE_SCOPE); } } Class getScriptClass(String script) throws SyntaxException, CompilationFailedException, IOException { Class clazz = classMap.get(script); if (clazz != null) { return clazz; } clazz = loader.parseClass(script, generateScriptName()); classMap.put(script, clazz); return clazz; } public void setClassLoader(final GroovyClassLoader classLoader) { this.loader = classLoader; } public GroovyClassLoader getClassLoader() { return this.loader; } //-- Internals only below this point // invokes the specified method/function on the given object. private Object invokeImpl(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException { if (name == null) { throw new NullPointerException("method name is null"); } try { if (thiz != null) { return InvokerHelper.invokeMethod(thiz, name, args); } else { return callGlobal(name, args); } } catch (MissingMethodException mme) { throw new NoSuchMethodException(mme.getMessage()); } catch (Exception e) { throw new ScriptException(e); } } // call the script global function of the given name private Object callGlobal(String name, Object[] args) { return callGlobal(name, args, context); } private Object callGlobal(String name, Object[] args, ScriptContext ctx) { Closure closure = globalClosures.get(name); if (closure != null) { return closure.call(args); } else { // Look for closure valued variable in the // given ScriptContext. If available, call it. Object value = ctx.getAttribute(name); if (value instanceof Closure) { return ((Closure) value).call(args); } // else fall thru.. } throw new MissingMethodException(name, getClass(), args); } // generate a unique name for top-level Script classes private synchronized String generateScriptName() { return "Script" + (++counter) + ".groovy"; } private <T> T makeInterface(Object obj, Class<T> clazz) { final Object thiz = obj; if (clazz == null || !clazz.isInterface()) { throw new IllegalArgumentException("interface Class expected"); } return (T) Proxy.newProxyInstance( clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() { public Object invoke(Object proxy, Method m, Object[] args) throws Throwable { return invokeImpl(thiz, m.getName(), args); } }); } // determine appropriate class loader to serve as parent loader // for GroovyClassLoader instance private ClassLoader getParentLoader() { // check whether thread context loader can "see" Groovy Script class ClassLoader ctxtLoader = Thread.currentThread().getContextClassLoader(); try { Class c = ctxtLoader.loadClass(Script.class.getName()); if (c == Script.class) { return ctxtLoader; } } catch (ClassNotFoundException cnfe) { /* ignore */ } // exception was thrown or we get wrong class return Script.class.getClassLoader(); } 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(); } }