/* * 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. */ /* * GroovyScriptEngine.java * @author Mike Grogan * @author A. Sundararajan */ package org.jbpm.pvm.internal.script; import java.io.*; import java.util.*; import javax.script.*; import groovy.lang.*; 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 org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.CompilationFailedException; import java.lang.reflect.*; public class GroovyScriptEngine extends AbstractScriptEngine implements Compilable, Invocable { private static boolean DEBUG = false; // script-string-to-generated Class map private Map classMap; // global closures map - this is used to simulate a single // global functions namespace private Map 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 GroovyScriptEngine() { classMap = Collections.synchronizedMap(new HashMap()); globalClosures = Collections.synchronizedMap(new HashMap()); 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 { return eval(getScriptClass(script), 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.Invocable 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 getInterface(Class clasz) { return makeInterface(null, clasz); } public T getInterface(Object thiz, Class clasz) { if (thiz == null) { throw new IllegalArgumentException("script object is null"); } return makeInterface(thiz, clasz); } // package-privates Object eval(Class scriptClass, final ScriptContext ctx) throws ScriptException { //add context to bindings ctx.setAttribute("context", ctx, ScriptContext.ENGINE_SCOPE); //direct output to ctx.getWriter Writer writer = ctx.getWriter(); ctx.setAttribute("out", (writer instanceof PrintWriter) ? writer : new PrintWriter(writer), 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 { Script scriptObject = InvokerHelper.createScript(scriptClass, binding); // create a Map of MethodClosures from this new script object Method[] methods = scriptClass.getMethods(); Map closures = new HashMap(); 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); } } Class getScriptClass(String script) throws SyntaxException, CompilationFailedException, IOException { Class clazz = classMap.get(script); if (clazz != null) { return clazz; } InputStream stream = new ByteArrayInputStream(script.getBytes()); clazz = loader.parseClass(stream, generateScriptName()); classMap.put(script, clazz); return clazz; } //-- 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 makeInterface(Object obj, Class 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("org.codehaus.groovy.Script"); if (c == Script.class) { return ctxtLoader; } } catch (ClassNotFoundException cnfe) { } // 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(); } }