/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2006-2011 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
/*
* Portions Copyright 2012 RHQ Management Platform
*/
/*
* RHQ Management Platform elects to include this software in this distribution
* under the GPL Version 2 license.
*/
package org.rhq.scripting.javascript.engine;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
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 javax.script.SimpleScriptContext;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.ConsString;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.LazilyLoadedCtor;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Synchronizer;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.WrapFactory;
import org.mozilla.javascript.Wrapper;
import org.mozilla.javascript.commonjs.module.RequireBuilder;
import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProvider;
import org.mozilla.javascript.commonjs.module.provider.SoftCachingModuleScriptProvider;
import org.mozilla.javascript.commonjs.module.provider.UrlModuleSourceProvider;
import org.rhq.scripting.javascript.engine.util.ExtendedScriptException;
import org.rhq.scripting.javascript.engine.util.InterfaceImplementor;
/**
* Implementation of <code>ScriptEngine</code> using the Mozilla Rhino
* interpreter.
*
* @author Mike Grogan
* @author A. Sundararajan
* @version 1.0
* @since 1.6
*
* Modified for phobos to remove some of the restrictions.
* Modified to allow subclassing and preprocessing of script source code.
* Modified to avoid using the RhinoTopLevel class, since that introduces
* a circularity that prevents objects from being garbage collected.
*
* @author Roberto Chinnici
*
* Modified so that the top level scope is an ImportTopLevel instance so
* that importClass and importPackage functions are available.
* Modified so that the "print" and "println" functions work the same as with
* the stock javascript script engine provided by the JVM.
* Modified to include the "require()" function by default.
* Modified to tighten the security of the script execution by running it in an
* AccessControlContext active at the time of the script engine creation.
* Modified to allow correct interoperability between Java and javascript string even
* if represented by the custom ConsString instance.
*
* @author Lukas Krejci
*/
public class RhinoScriptEngine extends AbstractScriptEngine
implements Invocable, Compilable {
public static final boolean DEBUG = false;
private static final String TOPLEVEL_SCRIPT_NAME = "META-INF/toplevel.js";
private static class TopLevelScope extends ImporterTopLevel {
private static final long serialVersionUID = 1L;
private AccessControlContext acc;
public TopLevelScope(AccessControlContext acc, Context cx, boolean sealed) {
super(cx, sealed);
this.acc = acc;
}
public AccessControlContext getAccessControlContext() {
return acc;
}
}
/* Scope where standard JavaScript objects and our
* extensions to it are stored. Note that these are not
* user defined engine level global variables. These are
* variables have to be there on all compliant ECMAScript
* scopes. We put these standard objects in this top level.
*/
private TopLevelScope topLevel;
/* map used to store indexed properties in engine scope
* refer to comment on 'indexedProps' in ExternalScriptable.java.
*/
private Map<?, ?> indexedProps;
private ScriptEngineFactory factory;
private InterfaceImplementor implementor;
//LK - added support for CommonJS modules
private RequireBuilder requireBuilder;
//LK - custom wrap factory to overcome the difficulties comparing java strings with ConsString instances
// introduced by Rhino 1.7R4.
private static class CustomWrapFactory extends WrapFactory {
/**
* This behaves exactly the same as the super class' method except the fact that
* the ConsString is considered "primitive" and is not wrapped in any manner.
* <p>
* This is then consistent with the rest of Rhino that expects ConsString as a possible
* implementation of the string.
*/
@Override
public Object wrap(Context cx, Scriptable scope, Object obj, Class<?> staticType) {
if (obj instanceof ConsString) {
return obj;
}
return super.wrap(cx, scope, obj, staticType);
}
}
//LK - make all the scripts run in an access control context
//LK - use a custom wrap factory to overcome the ConsString being mishandled when transferring from java to js and back
static {
ContextFactory.initGlobal(new ContextFactory() {
@Override
protected Object doTopCall(final Callable callable,
final Context cx, final Scriptable scope,
final Scriptable thisObj, final Object[] args) {
AccessControlContext accCtxt = null;
Scriptable global = ScriptableObject.getTopLevelScope(scope);
Scriptable globalProto = global.getPrototype();
if (globalProto instanceof TopLevelScope) {
accCtxt = ((TopLevelScope)globalProto).getAccessControlContext();
}
if (accCtxt != null) {
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
return superDoTopCall(callable, cx, scope, thisObj, args);
}
}, accCtxt);
} else {
return superDoTopCall(callable, cx, scope, thisObj, args);
}
}
@Override
protected Context makeContext() {
Context cx = super.makeContext();
cx.setOptimizationLevel(-1);
cx.setWrapFactory(new CustomWrapFactory());
return cx;
}
private Object superDoTopCall(final Callable callable,
final Context cx, final Scriptable scope,
final Scriptable thisObj, final Object[] args) {
return super.doTopCall(callable, cx, scope, thisObj, args);
}
});
}
/*
// in Phobos we want to support all javascript features
static {
ContextFactory.initGlobal(new ContextFactory() {
protected Context makeContext() {
Context cx = super.makeContext();
cx.setClassShutter(RhinoClassShutter.getInstance());
cx.setWrapFactory(RhinoWrapFactory.getInstance());
return cx;
}
public boolean hasFeature(Context cx, int feature) {
// we do not support E4X (ECMAScript for XML)!
if (feature == Context.FEATURE_E4X) {
return false;
} else {
return super.hasFeature(cx, feature);
}
}
});
}
static {
if (USE_INTERPRETER) {
ContextFactory.initGlobal(new ContextFactory() {
protected Context makeContext() {
Context cx = super.makeContext();
cx.setOptimizationLevel(-1);
return cx;
}
});
}
}
*/
public RhinoScriptEngine() {
this(new UrlModuleSourceProvider(null, Arrays.asList(new File("./").toURI())));
}
/**
* Creates a new instance of RhinoScriptEngine with given moduleSourceProvider as the "locator"
* for the CommonJS modules.
*
* @param moduleSourceProvider the implementation able to locate sources of modules for CommonJS.
*/
public RhinoScriptEngine(ModuleSourceProvider moduleSourceProvider) {
Context cx = enterContext();
try {
/*
* RRC - modified this code to register JSAdapter and some functions
* directly, without using a separate RhinoTopLevel class
*/
/*
* LK - made the topLevel at the the ImporterTopLevel so that
* the circular reference to this script engine is avoided but
* all the functions (importClass, importPackage) are available.
* Also provide the security features similar to the bundled script engine.
*/
topLevel = new TopLevelScope(AccessController.getContext(), cx, System.getSecurityManager() != null);
requireBuilder = new RequireBuilder();
setModuleSourceProvider(moduleSourceProvider);
requireBuilder.setSandboxed(false);
new LazilyLoadedCtor(topLevel, "JSAdapter",
"org.rhq.scripting.javascript.engine.JSAdapter",
false);
// add top level functions
String names[] = { "bindings", "scope", "sync" };
topLevel.defineFunctionProperties(names, RhinoScriptEngine.class, ScriptableObject.DONTENUM);
processAllTopLevelScripts(cx);
} finally {
Context.exit();
}
indexedProps = new HashMap<Object, Object>();
//construct object used to implement getInterface
implementor = new InterfaceImplementor(this) {
@Override
protected Object convertResult(Method method, Object res)
throws ScriptException {
Class<?> desiredType = method.getReturnType();
if (desiredType == Void.TYPE) {
return null;
} else {
return Context.jsToJava(res, desiredType);
}
}
};
}
public void setModuleSourceProvider(ModuleSourceProvider provider) {
requireBuilder.setModuleScriptProvider(new SoftCachingModuleScriptProvider(provider));
}
@Override
public Object eval(Reader reader, ScriptContext ctxt)
throws ScriptException {
Object ret;
Context cx = enterContext();
try {
Scriptable scope = getRuntimeScope(ctxt);
scope.put("context", scope, ctxt);
// NOTE (RRC) - why does it look straight into the engine instead of asking
// the given ScriptContext object?
// Modified to use the context
// String filename = (String) get(ScriptEngine.FILENAME);
String filename = null;
if (ctxt != null && ctxt.getBindings(ScriptContext.ENGINE_SCOPE) != null) {
filename = (String) ctxt.getBindings(ScriptContext.ENGINE_SCOPE).get(RhinoScriptEngine.FILENAME);
}
if (filename == null) {
filename = (String) get(RhinoScriptEngine.FILENAME);
}
filename = filename == null ? "<Unknown source>" : filename;
ret = cx.evaluateReader(scope, preProcessScriptSource(reader), filename , 1, null);
} catch (JavaScriptException jse) {
if (DEBUG) jse.printStackTrace();
int line = (line = jse.lineNumber()) == 0 ? -1 : line;
Object value = jse.getValue();
String str = (value != null && value.getClass().getName().equals("org.mozilla.javascript.NativeError") ?
value.toString() :
jse.toString());
throw new ExtendedScriptException(jse, str, jse.sourceName(), line);
} catch (RhinoException re) {
if (DEBUG) re.printStackTrace();
int line = (line = re.lineNumber()) == 0 ? -1 : line;
throw new ExtendedScriptException(re, re.toString(), re.sourceName(), line);
} catch (IOException ee) {
throw new ScriptException(ee);
} finally {
Context.exit();
}
return unwrapReturnValue(ret);
}
@Override
public Object eval(String script, ScriptContext ctxt) throws ScriptException {
if (script == null) {
throw new NullPointerException("null script");
}
return eval(new StringReader(script), ctxt);
}
@Override
public ScriptEngineFactory getFactory() {
if (factory != null) {
return factory;
} else {
return new RhinoScriptEngineFactory();
}
}
@Override
public Bindings createBindings() {
return new SimpleBindings();
}
//Invocable methods
@Override
public Object invokeFunction(String name, Object... args)
throws ScriptException, NoSuchMethodException {
return invokeMethod(null, name, args);
}
@Override
public Object invokeMethod(Object thiz, String name, Object... args)
throws ScriptException, NoSuchMethodException {
Context cx = enterContext();
try {
if (name == null) {
throw new NullPointerException("method name is null");
}
if (thiz != null && !(thiz instanceof Scriptable)) {
thiz = Context.toObject(thiz, topLevel);
}
Scriptable engineScope = getRuntimeScope(context);
Scriptable localScope = (thiz != null)? (Scriptable) thiz :
engineScope;
Object obj = ScriptableObject.getProperty(localScope, name);
if (! (obj instanceof Function)) {
throw new NoSuchMethodException("no such method: " + name);
}
Function func = (Function) obj;
Scriptable scope = func.getParentScope();
if (scope == null) {
scope = engineScope;
}
Object result = func.call(cx, scope, localScope,
wrapArguments(args));
return unwrapReturnValue(result);
} catch (JavaScriptException jse) {
if (DEBUG) jse.printStackTrace();
int line = (line = jse.lineNumber()) == 0 ? -1 : line;
Object value = jse.getValue();
String str = (value != null && value.getClass().getName().equals("org.mozilla.javascript.NativeError") ?
value.toString() :
jse.toString());
throw new ExtendedScriptException(jse, str, jse.sourceName(), line);
} catch (RhinoException re) {
if (DEBUG) re.printStackTrace();
int line = (line = re.lineNumber()) == 0 ? -1 : line;
throw new ExtendedScriptException(re, re.toString(), re.sourceName(), line);
} finally {
Context.exit();
}
}
@Override
public <T> T getInterface(Class<T> clasz) {
try {
return implementor.getInterface(null, clasz);
} catch (ScriptException e) {
return null;
}
}
@Override
public <T> T getInterface(Object thiz, Class<T> clasz) {
if (thiz == null) {
throw new IllegalArgumentException("script object can not be null");
}
try {
return implementor.getInterface(thiz, clasz);
} catch (ScriptException e) {
return null;
}
}
// RRC - not used
// LK - make it used again and modified to conform to the JVM version.
private static final String printSource =
"function print(str, newline) { \n" +
" if (typeof(str) == 'undefined') { \n" +
" str = 'undefined'; \n" +
" } else if (str == null) { \n" +
" str = 'null'; \n" +
" } \n" +
" var out = context.getWriter(); \n" +
" out.print(String(str)); \n" +
" if (newline) out.print('\\n'); \n" +
" out.flush(); \n" +
"}\n" +
"function println(str) { \n" +
" print(str, true); \n" +
"}";
Scriptable getRuntimeScope(ScriptContext ctxt) {
if (ctxt == null) {
throw new NullPointerException("null script context");
}
// we create a scope for the given ScriptContext
Scriptable newScope = new ExternalScriptable(ctxt, indexedProps);
// Set the prototype of newScope to be 'topLevel' so that
// JavaScript standard objects are visible from the scope.
newScope.setPrototype(topLevel);
// define "context" variable in the new scope
newScope.put("context", newScope, ctxt);
// RRC - save some time and don't define print
// LK - these functions are assumed by a lot of code so let's
// make them available
// define "print" function in the new scope
Context cx = enterContext();
try {
cx.evaluateString(newScope, printSource, "print", 1, null);
requireBuilder.createRequire(cx, newScope).install(newScope);
} finally {
Context.exit();
}
return newScope;
}
//Compilable methods
@Override
public CompiledScript compile(String script) throws ScriptException {
return compile(new StringReader(script));
}
@Override
public CompiledScript compile(java.io.Reader script) throws ScriptException {
CompiledScript ret = null;
Context cx = enterContext();
try {
String filename = (String) get(RhinoScriptEngine.FILENAME);
if (filename == null) {
filename = "<Unknown Source>";
}
Script scr = cx.compileReader(preProcessScriptSource(script), filename, 1, null);
ret = new RhinoCompiledScript(this, scr);
} catch (Exception e) {
if (DEBUG) e.printStackTrace();
throw new ScriptException(e);
} finally {
Context.exit();
}
return ret;
}
//package-private helpers
static Context enterContext() {
// call this always so that initializer of this class runs
// and initializes custom wrap factory and class shutter.
return Context.enter();
}
void setEngineFactory(ScriptEngineFactory fac) {
factory = fac;
}
Object[] wrapArguments(Object[] args) {
if (args == null) {
return Context.emptyArgs;
}
Object[] res = new Object[args.length];
for (int i = 0; i < res.length; i++) {
res[i] = Context.javaToJS(args[i], topLevel);
}
return res;
}
Object unwrapReturnValue(Object result) {
if (result instanceof Wrapper) {
result = ( (Wrapper) result).unwrap();
}
return result instanceof Undefined ? null : result;
}
protected Reader preProcessScriptSource(Reader reader) throws ScriptException {
return reader;
}
protected void processAllTopLevelScripts(Context cx) {
processTopLevelScript(TOPLEVEL_SCRIPT_NAME, cx);
}
protected void processTopLevelScript(String scriptName, Context cx) {
InputStream toplevelScript = this.getClass().getClassLoader().getResourceAsStream(scriptName);
if (toplevelScript != null) {
Reader reader = new InputStreamReader(toplevelScript);
try {
cx.evaluateReader(topLevel, reader, scriptName, 1, null);
}
catch (Exception e) {
if (DEBUG) e.printStackTrace();
}
finally {
try {
toplevelScript.close();
}
catch (IOException e) {
}
}
}
}
/**
* The bindings function takes a JavaScript scope object
* of type ExternalScriptable and returns the underlying Bindings
* instance.
*
* var page = scope(pageBindings);
* with (page) {
* // code that uses page scope
* }
* var b = bindings(page);
* // operate on bindings here.
*/
public static Object bindings(Context cx, Scriptable thisObj, Object[] args,
Function funObj) {
if (args.length == 1) {
Object arg = args[0];
if (arg instanceof Wrapper) {
arg = ((Wrapper)arg).unwrap();
}
if (arg instanceof ExternalScriptable) {
ScriptContext ctx = ((ExternalScriptable)arg).getContext();
Bindings bind = ctx.getBindings(ScriptContext.ENGINE_SCOPE);
return Context.javaToJS(bind,
ScriptableObject.getTopLevelScope(thisObj));
}
}
return Context.getUndefinedValue();
}
/**
* The scope function creates a new JavaScript scope object
* with given Bindings object as backing store. This can be used
* to create a script scope based on arbitrary Bindings instance.
* For example, in webapp scenario, a 'page' level Bindings instance
* may be wrapped as a scope and code can be run in JavaScripe 'with'
* statement:
*
* var page = scope(pageBindings);
* with (page) {
* // code that uses page scope
* }
*/
public static Object scope(Context cx, Scriptable thisObj, Object[] args,
Function funObj) {
if (args.length == 1) {
Object arg = args[0];
if (arg instanceof Wrapper) {
arg = ((Wrapper)arg).unwrap();
}
if (arg instanceof Bindings) {
ScriptContext ctx = new SimpleScriptContext();
ctx.setBindings((Bindings)arg, ScriptContext.ENGINE_SCOPE);
Scriptable res = new ExternalScriptable(ctx);
res.setPrototype(ScriptableObject.getObjectPrototype(thisObj));
res.setParentScope(ScriptableObject.getTopLevelScope(thisObj));
return res;
}
}
return Context.getUndefinedValue();
}
/**
* The sync function creates a synchronized function (in the sense
* of a Java synchronized method) from an existing function. The
* new function synchronizes on the <code>this</code> object of
* its invocation.
* js> var o = { f : sync(function(x) {
* print("entry");
* Packages.java.lang.Thread.sleep(x*1000);
* print("exit");
* })};
* js> thread(function() {o.f(5);});
* entry
* js> thread(function() {o.f(5);});
* js>
* exit
* entry
* exit
*/
public static Object sync(Context cx, Scriptable thisObj, Object[] args,
Function funObj) {
if (args.length == 1 && args[0] instanceof Function) {
return new Synchronizer((Function)args[0]);
} else {
throw Context.reportRuntimeError("wrong argument(s) for sync");
}
}
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("No file specified");
return;
}
InputStreamReader r = new InputStreamReader(new FileInputStream(args[0]));
RhinoScriptEngine engine = new RhinoScriptEngine();
SimpleScriptContext context = new SimpleScriptContext();
engine.put(RhinoScriptEngine.FILENAME, args[0]);
engine.eval(r, context);
// added this statement to save some typing to most script authors
context.getWriter().flush();
}
}