/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
*---------------------------------------------------------------------------*
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.script.rhino;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Vector;
import java.util.Map;
import java.util.HashMap;
import org.apache.batik.bridge.InterruptedBridgeException;
import org.apache.batik.script.Interpreter;
import org.apache.batik.script.InterpreterException;
import org.apache.batik.script.Window;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.NativeJavaPackage;
import org.mozilla.javascript.PropertyException;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.SecurityController;
import org.mozilla.javascript.WrapFactory;
import org.mozilla.javascript.WrappedException;
import org.w3c.dom.events.EventTarget;
/**
* A simple implementation of <code>Interpreter</code> interface to use
* Rhino ECMAScript interpreter.
* @author <a href="mailto:cjolif@ilog.fr">Christophe Jolif</a>
* @version $Id: RhinoInterpreter.java,v 1.31 2003/07/10 13:49:59 deweese Exp $
*/
public class RhinoInterpreter implements Interpreter {
private static String[] TO_BE_IMPORTED = {
"java.lang",
"org.w3c.dom",
"org.w3c.dom.css",
"org.w3c.dom.events",
"org.w3c.dom.smil",
"org.w3c.dom.stylesheets",
"org.w3c.dom.svg",
"org.w3c.dom.views"
};
/**
* The window object
*/
protected Window window;
public Window getWindow() {
return window;
}
private static class Entry {
String str;
Script script;
Entry(String str, Script script) {
this.str = str;
this.script = script;
}
};
/**
* store last 32 precompiled objects.
*/
private static final int MAX_CACHED_SCRIPTS = 32;
/**
* Constant used to describe an SVG source
*/
public static final String SOURCE_NAME_SVG = "<SVG>";
/**
* Name of the "window" object when referenced by scripts
*/
public static final String BIND_NAME_WINDOW = "window";
private ScriptableObject globalObject = null;
private LinkedList compiledScripts = new LinkedList();
private WrapFactory wrapFactory =
new BatikWrapFactory(this);
/**
* The Rhino 'security domain'. We use the RhinoClassLoader
* which will grant permissions to connect to the document
* URL.
*/
protected RhinoClassLoader rhinoClassLoader;
/**
* The SecurityController implementation for Batik,
* which ensures scripts have access to the
* server they were downloaded from
*/
private SecurityController securityController
= new BatikSecurityController();
/**
* Build a <code>Interpreter</code> for ECMAScript using Rhino.
*
* @param documentURL the URL for the document which references
*
* @see org.apache.batik.script.Interpreter
* @see org.apache.batik.script.InterpreterPool
*/
public RhinoInterpreter(URL documentURL) {
rhinoClassLoader = new RhinoClassLoader(documentURL,
getClass().getClassLoader());
// entering a context
Context ctx = enterContext();
try {
try {
Scriptable scriptable = ctx.initStandardObjects(null, false);
ScriptableObject.defineClass(scriptable, WindowWrapper.class);
} catch (Exception e) {
// cannot happen
}
// we now have the window object as the global object from the
// launch of the interpreter.
// 1. it works around a Rhino bug introduced in 15R4 (but fixed
// by a later patch).
// 2. it sounds cleaner.
WindowWrapper wWrapper = new WindowWrapper(ctx);
globalObject = wWrapper;
// import Java lang package & DOM Level 2 & SVG DOM packages
NativeJavaPackage[] p= new NativeJavaPackage[TO_BE_IMPORTED.length];
for (int i = 0; i < TO_BE_IMPORTED.length; i++) {
p[i] = new NativeJavaPackage(TO_BE_IMPORTED[i]);
} try {
ScriptableObject.callMethod(globalObject, "importPackage", p);
} catch (JavaScriptException e) {
// cannot happen as we know the method is there and
// the parameters are ok
}
} finally {
Context.exit();
}
}
/**
* Returns the AccessControlContext associated with this Interpreter.
* @see org.apache.batik.script.rhino.RhinoClassLoader
*/
public AccessControlContext getAccessControlContext(){
return rhinoClassLoader.getAccessControlContext();
}
/**
* Implementation helper. Makes sure the proper security is set
* on the context.
*/
public Context enterContext(){
Context ctx = Context.getCurrentContext();
if (ctx == null) {
ctx = new ExtendedContext();
ctx.setWrapFactory(wrapFactory);
ctx.setSecurityController(securityController);
}
ctx = Context.enter(ctx);
return ctx;
}
/**
* This method returns the ECMAScript global object used by this interpreter.
*/
protected ScriptableObject getGlobalObject() {
return globalObject;
}
// org.apache.batik.script.Intepreter implementation
/**
* This method evaluates a piece of ECMAScript.
* @param scriptreader a <code>java.io.Reader</code> on the piece of script
* @return if no exception is thrown during the call, should return the
* value of the last expression evaluated in the script.
*/
public Object evaluate(Reader scriptreader)
throws InterpreterException, IOException {
return evaluate(scriptreader, SOURCE_NAME_SVG);
}
/**
* This method evaluates a piece of ECMAScript.
* @param scriptreader a <code>java.io.Reader</code> on the piece of script
* @param description description which can be later used (e.g., for error
* messages).
* @return if no exception is thrown during the call, should return the
* value of the last expression evaluated in the script.
*/
public Object evaluate(Reader scriptreader, String description)
throws InterpreterException, IOException {
Object rv = null;
final Context ctx = enterContext();
try {
rv = ctx.evaluateReader(globalObject,
scriptreader,
description,
1, rhinoClassLoader);
} catch (JavaScriptException e) {
// exception from JavaScript (possibly wrapping a Java Ex)
if (e.getValue() instanceof Exception) {
Exception ex = (Exception)e.getValue();
throw new InterpreterException(ex, ex.getMessage(), -1, -1);
} else
throw new InterpreterException(e, e.getMessage(), -1, -1);
} catch (WrappedException we) {
// main Rhino RuntimeException
Throwable w = we.getWrappedException();
if (w instanceof Exception)
throw
new InterpreterException((Exception)we.getWrappedException(),
we.getWrappedException().getMessage(),
-1, -1);
else
throw new InterpreterException(we.getWrappedException().getMessage(), -1, -1);
} catch (InterruptedBridgeException ibe) {
// This sometimes happens when script builds stuff.
throw ibe;
} catch (RuntimeException re) {
// other RuntimeExceptions
throw new InterpreterException(re, re.getMessage(), -1, -1);
} finally {
Context.exit();
}
return rv;
}
/**
* This method evaluates a piece of ECMA script.
* The first time a String is passed, it is compiled and evaluated.
* At next call, the piece of script will only be evaluated to
* prevent from recompiling it.
* @param scriptstr the piece of script
* @return if no exception is thrown during the call, should return the
* value of the last expression evaluated in the script.
*/
public Object evaluate(final String scriptstr)
throws InterpreterException {
Object rv = null;
final Context ctx = enterContext();
try {
Script script = null;
Entry et = null;
Iterator it = compiledScripts.iterator();
// between nlog(n) and log(n) because it is
// an AbstractSequentialList
while (it.hasNext()) {
if ((et = (Entry)(it.next())).str.equals(scriptstr)) {
// if it is not at the end, remove it because
// it will change from place (it is faster
// to remove it now)
script = et.script;
it.remove();
break;
}
}
if (script == null) {
// this script has not been compiled yet or has been forgotten
// since the compilation:
// compile it and store it for future use.
script = (Script)AccessController.doPrivileged
(new PrivilegedAction() {
public Object run() {
try {
return ctx.compileReader
(globalObject,
new StringReader(scriptstr),
SOURCE_NAME_SVG,
1, rhinoClassLoader);
} catch (IOException io) {
// Should never happen: using a string
throw new Error();
}
}
});
if (compiledScripts.size()+1 > MAX_CACHED_SCRIPTS) {
// too many cached items - we should delete the
// oldest entry. all of this is very fast on
// linkedlist
compiledScripts.removeFirst();
}
// stroring is done here:
compiledScripts.addLast(new Entry(scriptstr, script));
} else {
// this script has been compiled before,
// just update it's index so it won't get deleted soon.
compiledScripts.addLast(et);
}
try {
rv = script.exec(ctx, globalObject);
} catch (JavaScriptException e) {
// exception from JavaScript (possibly wrapping a Java Ex)
if (e.getValue() instanceof Exception) {
Exception ex = (Exception)e.getValue();
throw new InterpreterException(ex, ex.getMessage(), -1,-1);
} else
throw new InterpreterException(e, e.getMessage(), -1, -1);
} catch (WrappedException we) {
// main Rhino RuntimeException
throw
new InterpreterException
((Exception)we.getWrappedException(),
we.getWrappedException().getMessage(), -1, -1);
} catch (RuntimeException re) {
// other RuntimeExceptions
throw new InterpreterException(re, re.getMessage(), -1, -1);
}
} finally {
Context.exit();
}
return rv;
}
/**
* For <code>RhinoInterpreter</code> this method does nothing.
*/
public void dispose() {
Context.setCachingEnabled(false);
Context.setCachingEnabled(true);
}
/**
* This method registers a particular Java <code>Object</code> in
* the environment of the interpreter.
* @param name the name of the script object to create
* @param object the Java object
*/
public void bindObject(String name, Object object) {
Context ctx = enterContext();
try {
if (name.equals(BIND_NAME_WINDOW) && object instanceof Window) {
window = (Window)object;
object = globalObject;
}
try {
Scriptable jsObject;
jsObject = Context.toObject(object, globalObject);
objects.put(name, jsObject);
if (ScriptableObject.getProperty(globalObject, name) ==
ScriptableObject.NOT_FOUND)
globalObject.defineProperty
(name, new RhinoGetDelegate(name),
rhinoGetter, null, ScriptableObject.READONLY);
} catch (PropertyException pe) {
pe.printStackTrace();
}
} finally {
Context.exit();
}
}
/**
* HashTable to store properties bounds on the global object.
* So they don't end up in the JavaMethods static table.
*/
Map objects = new HashMap(4);
/**
* Class to act as 'get' delegate for Rhino. This uses the
* currentContext to get the current Interpreter object which
* allows it to lookup the object requested. This gets around the
* fact that the global object gets referenced from a static
* context but the Context does not.
*/
public static class RhinoGetDelegate {
String name;
RhinoGetDelegate(String name) {
this.name = name;
}
public Object get(ScriptableObject so) {
Context ctx = Context.getCurrentContext();
if (ctx == null ) return null;
return ((ExtendedContext)ctx).getInterpreter().objects.get(name);
}
}
// The method to use for getting the value from the
// RhinoGetDelegate.
static Method rhinoGetter;
static {
try {
Class [] getterArgs = { ScriptableObject.class };
rhinoGetter = RhinoGetDelegate.class.getDeclaredMethod
("get", getterArgs);
} catch (NoSuchMethodException nsm) { }
}
/**
* To be used by <code>EventTargetWrapper</code>.
*/
void callHandler(Function handler,
Object arg)
throws JavaScriptException {
Context ctx = enterContext();
try {
arg = Context.toObject(arg, globalObject);
Object[] args = {arg};
handler.call(ctx, globalObject, globalObject, args);
} finally {
Context.exit();
}
}
/**
* To be used by <code>WindowWrapper</code>.
*/
void callMethod(ScriptableObject obj,
String methodName,
ArgumentsBuilder ab)
throws JavaScriptException {
Context ctx = enterContext();
try {
ScriptableObject.callMethod(obj, methodName, ab.buildArguments());
} finally {
Context.exit();
}
}
/**
* To be used by <code>WindowWrapper</code>.
*/
void callHandler(Function handler,
Object[] args)
throws JavaScriptException {
Context ctx = enterContext();
try {
handler.call(ctx, globalObject, globalObject, args);
} finally {
Context.exit();
}
}
/**
* To be used by <code>WindowWrapper</code>.
*/
void callHandler(Function handler,
ArgumentsBuilder ab)
throws JavaScriptException {
Context ctx = enterContext();
try {
handler.call(ctx, globalObject, globalObject, ab.buildArguments());
} finally {
Context.exit();
}
}
/**
* To build an argument list.
*/
public interface ArgumentsBuilder {
Object[] buildArguments();
}
/**
* Build the wrapper for objects implement <code>EventTarget</code>.
*/
Scriptable buildEventTargetWrapper(EventTarget obj) {
return new EventTargetWrapper(globalObject, obj);
}
/**
* By default Rhino has no output method in its language. That's why
* this method does nothing.
* @param output the new out <code>Writer</code>.
*/
public void setOut(Writer out) {
// no implementation of a default output function in Rhino
}
// org.apache.batik.i18n.Localizable implementation
/**
* Provides a way to the user to specify a locale which override the
* default one. If null is passed to this method, the used locale
* becomes the global one.
* @param l The locale to set.
*/
public Locale getLocale() {
// <!> TODO : in Rhino the local is for a thread not a scope..
return null;
}
/**
* Returns the current locale or null if the locale currently used is
* the default one.
*/
public void setLocale(Locale locale) {
// <!> TODO : in Rhino the local is for a thread not a scope..
}
/**
* Creates and returns a localized message, given the key of the message, 0, data.length
* in the resource bundle and the message parameters.
* The messages in the resource bundle must have the syntax described in
* the java.text.MessageFormat class documentation.
* @param key The key used to retreive the message from the resource
* bundle.
* @param args The objects that compose the message.
* @exception MissingResourceException if the key is not in the bundle.
*/
public String formatMessage(String key, Object[] args) {
return null;
}
public class ExtendedContext extends Context {
public ExtendedContext() {
super();
}
public RhinoInterpreter getInterpreter() {
return RhinoInterpreter.this;
}
public Window getWindow() {
return RhinoInterpreter.this.getWindow();
}
public ScriptableObject getGlobalObject() {
return RhinoInterpreter.this.getGlobalObject();
}
}
}