/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.red5.server.script.rhino;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.regex.PatternSyntaxException;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scripting.ScriptCompilationException;
import org.springframework.util.ClassUtils;
/**
* Utility methods for handling Rhino / Javascript objects.
*
* @author Paul Gregoire
* @since 0.6
*/
public class RhinoScriptUtils {
private static final Logger log = LoggerFactory.getLogger(RhinoScriptUtils.class);
// ScriptEngine manager
private static ScriptEngineManager mgr = new ScriptEngineManager();
// Javascript wrapper
private static final String jsWrapper = "function Wrapper(obj){return new JSAdapter(){ __has__ : function(name){return true;}, __get__ : function(name){if (name in obj){return obj[name];}else if (typeof(obj['doesNotUnderstand']) == 'function'){return function(){return obj.doesNotUnderstand(name, arguments);}}else{return undefined;}}};}";
/**
* Create a new Rhino-scripted object from the given script source.
*
* @param scriptSource
* the script source text
* @param interfaces
* the interfaces that the scripted Java object is supposed to
* implement
* @param extendedClass
* @return the scripted Java object
* @throws ScriptCompilationException
* in case of Rhino parsing failure
* @throws java.io.IOException
*/
@SuppressWarnings("rawtypes")
public static Object createRhinoObject(String scriptSource, Class[] interfaces, Class extendedClass) throws ScriptCompilationException, IOException, Exception {
if (log.isDebugEnabled()) {
log.debug("Script Engine Manager: " + mgr.getClass().getName());
}
ScriptEngine engine = mgr.getEngineByExtension("js");
if (null == engine) {
log.warn("Javascript is not supported in this build");
}
// set engine scope namespace
Bindings nameSpace = engine.getBindings(ScriptContext.ENGINE_SCOPE);
// add the logger to the script
nameSpace.put("log", log);
// compile the wrapper script
CompiledScript wrapper = ((Compilable) engine).compile(jsWrapper);
nameSpace.put("Wrapper", wrapper);
// get the function name ie. class name / ctor
String funcName = RhinoScriptUtils.getFunctionName(scriptSource);
if (log.isDebugEnabled()) {
log.debug("New script: " + funcName);
}
// set the 'filename'
nameSpace.put(ScriptEngine.FILENAME, funcName);
if (null != interfaces) {
nameSpace.put("interfaces", interfaces);
}
if (null != extendedClass) {
if (log.isDebugEnabled()) {
log.debug("Extended: " + extendedClass.getName());
}
nameSpace.put("supa", extendedClass.newInstance());
}
//
// compile the script
CompiledScript script = ((Compilable) engine).compile(scriptSource);
// eval the script with the associated namespace
Object o = null;
try {
o = script.eval();
} catch (Exception e) {
log.error("Problem evaluating script", e);
}
if (log.isDebugEnabled()) {
log.debug("Result of script call: " + o);
}
// script didnt return anything we can use so try the wrapper
if (null == o) {
wrapper.eval();
} else {
wrapper.eval();
o = ((Invocable) engine).invokeFunction("Wrapper", new Object[] { engine.get(funcName) });
if (log.isDebugEnabled()) {
log.debug("Result of invokeFunction: " + o);
}
}
return Proxy.newProxyInstance(ClassUtils.getDefaultClassLoader(), interfaces, new RhinoObjectInvocationHandler(engine, o));
}
/**
* InvocationHandler that invokes a Rhino script method.
*/
private static class RhinoObjectInvocationHandler implements InvocationHandler {
private final ScriptEngine engine;
private final Object instance;
public RhinoObjectInvocationHandler(ScriptEngine engine, Object instance) {
this.engine = engine;
this.instance = instance;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object o = null;
// ensure a set of args are available
if (args == null || args.length == 0) {
args = new Object[] { "" };
}
String name = method.getName();
if (log.isDebugEnabled()) {
log.debug("Calling: " + name);
}
try {
@SuppressWarnings("unused")
Method apiMethod = null;
Invocable invocable = (Invocable) engine;
if (null == instance) {
o = invocable.invokeFunction(name, args);
} else {
try {
o = invocable.invokeMethod(instance, name, args);
} catch (NoSuchMethodException nex) {
log.debug("Method not found: " + name);
try {
// try to invoke it directly, this will work if the
// function is in the engine context
// ie. the script has been already evaluated
o = invocable.invokeFunction(name, args);
} catch (Exception ex) {
log.debug("Function not found: " + name);
Class[] interfaces = (Class[]) engine.get("interfaces");
for (Class clazz : interfaces) {
// java6 style
o = invocable.getInterface(engine.get((String) engine.get("className")), clazz);
if (null != o) {
log.debug("Interface return type: " + o.getClass().getName());
break;
}
}
}
}
}
if (log.isDebugEnabled()) {
log.debug("Invocable result: " + o);
}
} catch (NoSuchMethodException nex) {
log.warn("Method not found");
} catch (Throwable t) {
log.warn("{}", t);
}
return o;
}
}
/**
* Uses a regex to get the first "function" name, this name is used to
* create an instance of the javascript object.
*
* @param scriptSource
* @return function name
*/
private static String getFunctionName(String scriptSource) {
String ret = "undefined";
try {
ret = scriptSource.replaceAll("[\\S\\w\\s]*?function ([\\w]+)\\(\\)[\\S\\w\\s]+", "$1");
// if undefined then look for the first var
if (ret.equals("undefined") || ret.length() > 64) {
ret = scriptSource.replaceAll("[\\S\\w\\s]*?var ([\\w]+)[\\S\\w\\s]+", "$1");
}
} catch (PatternSyntaxException ex) {
log.error("Syntax error in the regular expression");
} catch (IllegalArgumentException ex) {
log.error("Syntax error in the replacement text (unescaped $ signs?)");
} catch (IndexOutOfBoundsException ex) {
log.error("Non-existent backreference used the replacement text");
}
log.debug("Got a function name: {}", ret);
return ret;
}
}