helma/src/helma/scripting/rhino/RhinoCore.java

942 lines
32 KiB
Java

/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2003 Helma Software. All Rights Reserved.
*
* $RCSfile$
* $Author$
* $Revision$
* $Date$
*/
package helma.scripting.rhino;
import helma.scripting.rhino.extensions.*;
import helma.framework.*;
import helma.framework.core.*;
import helma.main.Server;
import helma.objectmodel.*;
import helma.objectmodel.db.DbMapping;
import helma.objectmodel.db.Relation;
import helma.scripting.*;
import helma.util.CacheMap;
import helma.util.SystemMap;
import helma.util.WrappedMap;
import helma.util.SystemProperties;
import helma.util.Updatable;
import org.mozilla.javascript.*;
import java.io.*;
import java.text.*;
import java.util.*;
/**
* This is the implementation of ScriptingEnvironment for the Mozilla Rhino EcmaScript interpreter.
*/
public final class RhinoCore {
// the application we're running in
public final Application app;
// the global object
GlobalObject global;
// caching table for JavaScript object wrappers
CacheMap wrappercache;
// table containing JavaScript prototypes
Hashtable prototypes;
long lastUpdate = 0;
// the wrap factory
WrapFactory wrapper;
// the prototype for path objects
PathWrapper pathProto;
// Any error that may have been found in global code
EcmaError globalError;
/**
* Create a Rhino evaluator for the given application and request evaluator.
*/
public RhinoCore(Application app) {
this.app = app;
// wrappercache = new CacheMap(500, .75f);
wrappercache = new CacheMap(500);
prototypes = new Hashtable();
Context context = Context.enter();
context.setCompileFunctionsWithDynamicScope(true);
wrapper = new WrapMaker();
wrapper.setJavaPrimitiveWrap(false);
context.setWrapFactory(wrapper);
int optLevel = 0;
try {
optLevel = Integer.parseInt(app.getProperty("rhino.optlevel"));
} catch (Exception ignore) {
}
// System.err.println("Setting Rhino optlevel to " + optLevel);
context.setOptimizationLevel(optLevel);
try {
GlobalObject g = new GlobalObject(this, app);
g.init();
global = (GlobalObject) context.initStandardObjects(g);
ScriptableObject.defineClass(global, HopObject.class);
ScriptableObject.defineClass(global, FileObject.class);
ScriptableObject.defineClass(global, FtpObject.class);
pathProto = new PathWrapper(this);
ImageObject.init(global);
XmlRpcObject.init(global);
MailObject.init(global, app.getProperties());
registerPrototype("hopobject",
(ScriptableObject) ScriptableObject
.getClassPrototype(global, "HopObject"));
registerPrototype("global", global);
// add some convenience functions to string, date and number prototypes
Scriptable stringProto = ScriptableObject.getClassPrototype(global, "String");
stringProto.put("trim", stringProto, new StringTrim());
Scriptable dateProto = ScriptableObject.getClassPrototype(global, "Date");
dateProto.put("format", dateProto, new DateFormat());
Scriptable numberProto = ScriptableObject.getClassPrototype(global, "Number");
numberProto.put("format", numberProto, new NumberFormat());
initialize();
} catch (Exception e) {
System.err.println("Cannot initialize interpreter");
System.err.println("Error: " + e);
e.printStackTrace();
throw new RuntimeException(e.getMessage());
} finally {
Context.exit();
}
}
/**
* Initialize the evaluator, making sure the minimum type information
* necessary to bootstrap the rest is parsed.
*/
private synchronized void initialize() {
Collection protos = app.getPrototypes();
for (Iterator i = protos.iterator(); i.hasNext();) {
Prototype proto = (Prototype) i.next();
initPrototype(proto);
}
// always fully initialize global prototype, because
// we always need it and there's no chance to trigger
// creation on demand.
getPrototype("global");
}
/**
* Initialize a prototype without fully parsing its script files.
*
* @param prototype the prototype to be created
*/
synchronized void initPrototype(Prototype prototype) {
String name = prototype.getName();
// check if the prototype info exists already
ScriptableObject op = getRawPrototype(name);
// if prototype info doesn't exist (i.e. is a standard prototype
// built by HopExtension), create it.
if (op == null) {
try {
op = new HopObject(name);
op.setParentScope(global);
// op.put("prototypename", op, name);
} catch (Exception ignore) {
System.err.println("Error creating prototype: " + ignore);
ignore.printStackTrace();
}
registerPrototype(name, op);
}
// Register a constructor for all types except global.
// This will first create a new prototyped hopobject and then calls
// the actual (scripted) constructor on it.
if (!"global".equalsIgnoreCase(name) &&
!"root".equalsIgnoreCase(name) &&
!"hopobject".equalsIgnoreCase(name)) {
try {
installConstructor(name, op);
} catch (Exception ignore) {
System.err.println("Error adding ctor for " + name + ": " + ignore);
ignore.printStackTrace();
}
}
}
/**
* Set up a prototype, parsing and compiling all its script files.
*
* @param prototype the prototype to update/evaluate/compile
* @param info the info, containing the object proto, last update time and
* the set of compiled functions properties
*/
synchronized void evaluatePrototype(Prototype prototype, TypeInfo info) {
// System.err.println("EVALUATING PROTO: "+prototype);
Scriptable op = info.objectPrototype;
// set the parent prototype in case it hasn't been done before
// or it has changed...
setParentPrototype(prototype, op);
globalError = info.error = null;
// loop through the prototype's code elements and evaluate them
// first the zipped ones ...
for (Iterator it = prototype.getZippedCode().values().iterator(); it.hasNext();) {
Object code = it.next();
evaluate(prototype, info, code);
}
// then the unzipped ones (this is to make sure unzipped code overwrites zipped code)
for (Iterator it = prototype.getCode().values().iterator(); it.hasNext();) {
Object code = it.next();
evaluate(prototype, info, code);
}
// loop through function properties defined for this proto and
// remove those which are left over from the previous generation
// and haven't been renewed in this pass.
Set oldFunctions = info.compiledFunctions;
Set newFunctions = new HashSet();
Object[] keys = ((ScriptableObject) op).getAllIds();
for (int i = 0; i < keys.length; i++) {
String key = keys[i].toString();
if (info.predefinedProperties.contains(key)) {
// don't mess with properties we didn't set
continue;
}
Object prop = op.get(key, op);
if (oldFunctions.contains(prop) && prop instanceof NativeFunction) {
// if this is a function compiled from script, it's from the
// old generation and wasn't renewed -- delete it.
// System.err.println("DELETING OLD FUNC: "+key);
try {
((ScriptableObject) op).setAttributes(key, op, 0);
op.delete(key);
} catch (PropertyException px) {
System.err.println("Error unsetting property "+key+" on "+prototype);
}
} else {
newFunctions.add(prop);
}
}
info.compiledFunctions = newFunctions;
info.lastUpdate = prototype.getLastUpdate();
}
/**
* Set the parent prototype on the ObjectPrototype.
*
* @param prototype the prototype spec
* @param op the prototype object
*/
private void setParentPrototype(Prototype prototype, Scriptable op) {
String name = prototype.getName();
if (!"global".equalsIgnoreCase(name) && !"hopobject".equalsIgnoreCase(name)) {
// get the prototype's prototype if possible and necessary
Scriptable opp = null;
Prototype parent = prototype.getParentPrototype();
if (parent != null) {
// see if parent prototype is already registered. if not, register it
opp = getPrototype(parent.getName());
}
if (opp == null && !app.isJavaPrototype(name)) {
// FIXME: does this ever occur?
opp = getPrototype("hopobject");
}
op.setPrototype(opp);
}
}
/**
* This is a version of org.mozilla.javascript.FunctionObject.addAsConstructor()
* that does not set the constructor property in the prototype. This is because
* we want our own scripted constructor function to prevail, if it is defined.
*
* @param name the name of the constructor
* @param op the object prototype
*/
private void installConstructor(String name, Scriptable op) {
FunctionObject fo = new FunctionObject(name, HopObject.hopObjCtor, global);
ScriptRuntime.setFunctionProtoAndParent(global, fo);
fo.setImmunePrototypeProperty(op);
op.setParentScope(fo);
ScriptableObject.defineProperty(global, name, fo, ScriptableObject.DONTENUM);
fo.setParentScope(global);
}
/**
* This method is called before an execution context is entered to let the
* engine know it should update its prototype information. The update policy
* here is to check for update those prototypes which already have been compiled
* before. Others will be updated/compiled on demand.
*/
public synchronized void updatePrototypes() {
if ((System.currentTimeMillis() - lastUpdate) < 1000L) {
return;
}
Collection protos = app.getPrototypes();
for (Iterator i = protos.iterator(); i.hasNext();) {
Prototype proto = (Prototype) i.next();
TypeInfo info = (TypeInfo) prototypes.get(proto.getName());
if (info == null) {
// a prototype we don't know anything about yet. Init local update info.
initPrototype(proto);
info = (TypeInfo) prototypes.get(proto.getName());
}
// only update prototype if it has already been initialized.
// otherwise, this will be done on demand
// System.err.println ("CHECKING PROTO "+proto+": "+info);
if (info.lastUpdate > 0) {
Prototype p = app.typemgr.getPrototype(info.protoName);
if (p != null) {
// System.err.println ("UPDATING PROTO: "+p);
app.typemgr.updatePrototype(p);
if (p.getLastUpdate() > info.lastUpdate) {
evaluatePrototype(p, info);
}
}
}
}
lastUpdate = System.currentTimeMillis();
}
/**
* Get a raw prototype, i.e. in potentially unfinished state
* without checking if it needs to be updated.
*/
private ScriptableObject getRawPrototype(String protoName) {
if (protoName == null) {
return null;
}
TypeInfo info = (TypeInfo) prototypes.get(protoName);
return (info == null) ? null : info.objectPrototype;
}
/**
* A version of getPrototype() that retrieves a prototype and checks
* if it is valid, i.e. there were no errors when compiling it. If
* invalid, a ScriptingException is thrown.
*/
public Scriptable getValidPrototype(String protoName) {
if (globalError != null) {
throw new RuntimeException(globalError.toString());
}
TypeInfo info = getPrototypeInfo(protoName);
if (info != null && info.error != null) {
throw new RuntimeException(info.error.toString());
}
return info == null ? null : info.objectPrototype;
}
/**
* Get the object prototype for a prototype name and initialize/update it
* if necessary. The policy here is to update the prototype only if it
* hasn't been updated before, otherwise we assume it already was updated
* by updatePrototypes(), which is called for each request.
*/
public Scriptable getPrototype(String protoName) {
TypeInfo info = getPrototypeInfo(protoName);
return info == null ? null : info.objectPrototype;
}
/**
* Private helper function that retrieves a prototype's TypeInfo
* and creates it if not yet created. This is used by getPrototype() and
* getValidPrototype().
*/
private TypeInfo getPrototypeInfo(String protoName) {
if (protoName == null) {
return null;
}
TypeInfo info = (TypeInfo) prototypes.get(protoName);
if ((info != null) && (info.lastUpdate == 0)) {
Prototype p = app.typemgr.getPrototype(protoName);
if (p != null) {
app.typemgr.updatePrototype(p);
if (p.getLastUpdate() > info.lastUpdate) {
evaluatePrototype(p, info);
}
// set info.lastUpdate to 1 if it is 0 so we know we
// have initialized this prototype already, even if
// it is empty (i.e. doesn't contain any scripts/skins/actions)
if (info.lastUpdate == 0) {
info.lastUpdate = 1;
}
}
}
return info;
}
/**
* Register an object prototype for a prototype name.
*/
private void registerPrototype(String protoName, ScriptableObject op) {
if ((protoName != null) && (op != null)) {
prototypes.put(protoName, new TypeInfo(op, protoName));
}
}
/**
* Check if an object has a function property (public method if it
* is a java object) with that name.
*/
public boolean hasFunction(String protoname, String fname) {
// System.err.println ("HAS_FUNC: "+fname);
try {
Scriptable op = getPrototype(protoname);
// if this is an untyped object return false
if (op == null) {
return false;
}
Object func = ScriptableObject.getProperty(op, fname);
if ((func != null) && func instanceof Function) {
return true;
}
} catch (Exception esx) {
// System.err.println ("Error in hasFunction: "+esx);
return false;
}
return false;
}
/**
* Convert an input argument from Java to the scripting runtime
* representation.
*/
public Object processXmlRpcArgument (Object what) throws Exception {
if (what == null)
return null;
if (what instanceof Vector) {
Vector v = (Vector) what;
Object[] a = v.toArray();
for (int i=0; i<a.length; i++) {
a[i] = processXmlRpcArgument(a[i]);
}
return Context.getCurrentContext().newArray(global, a);
}
if (what instanceof Hashtable) {
Hashtable t = (Hashtable) what;
for (Enumeration e=t.keys(); e.hasMoreElements(); ) {
Object key = e.nextElement();
t.put(key, processXmlRpcArgument(t.get(key)));
}
return Context.toObject(new SystemMap(t), global);
}
if (what instanceof String)
return what;
if (what instanceof Number)
return what;
if (what instanceof Boolean)
return what;
if (what instanceof Date) {
Date d = (Date) what;
Object[] args = { new Long(d.getTime()) };
return Context.getCurrentContext().newObject(global, "Date", args);
}
return Context.toObject(what, global);
}
/**
* convert a JavaScript Object object to a generic Java object stucture.
*/
public Object processXmlRpcResponse (Object what) throws Exception {
if (what instanceof Wrapper) {
what = ((Wrapper) what).unwrap();
}
if (what instanceof NativeObject) {
NativeObject no = (NativeObject) what;
Object[] ids = no.getIds();
Hashtable ht = new Hashtable(ids.length);
for (int i=0; i<ids.length; i++) {
if (ids[i] instanceof String) {
String key = (String) ids[i];
Object o = no.get(key, no);
if (o != null) {
ht.put(key, processXmlRpcResponse(o));
}
}
}
what = ht;
}
if (what instanceof NativeArray) {
NativeArray na = (NativeArray) what;
Number n = (Number) na.get("length", na);
int l = n.intValue();
Vector retval = new Vector(l);
for (int i=0; i<l; i++) {
retval.add(i, processXmlRpcResponse(na.get(i, na)));
}
what = retval;
}
if (what instanceof Number) {
Number n = (Number) what;
if (what instanceof Float || what instanceof Long) {
what = new Double(n.doubleValue());
} else if (!(what instanceof Double)) {
what = new Integer(n.intValue());
}
}
if (what instanceof Scriptable) {
Scriptable s = (Scriptable) what;
if ("Date".equals(s.getClassName())) {
what = new Date((long) ScriptRuntime.toNumber(s));
}
}
return what;
}
/**
* Return the application we're running in
*/
public Application getApplication() {
return app;
}
/**
* Get a Script wrapper for any given object. If the object implements the IPathElement
* interface, the getPrototype method will be used to retrieve the name of the prototype
* to use. Otherwise, a Java-Class-to-Script-Prototype mapping is consulted.
*/
public Scriptable getElementWrapper(Object e) {
Scriptable w = (Scriptable) wrappercache.get(e);
if (w == null) {
// Gotta find out the prototype name to use for this object...
String prototypeName = app.getPrototypeName(e);
Scriptable op = getPrototype(prototypeName);
if (op == null) {
prototypeName = "hopobject";
op = getPrototype("hopobject");
}
w = new JavaObject(global, e, prototypeName, op, this);
wrappercache.put(e, w);
}
return w;
}
/**
* Get a script wrapper for an instance of helma.objectmodel.INode
*/
public Scriptable getNodeWrapper(INode n) {
if (n == null) {
return null;
}
HopObject esn = (HopObject) wrappercache.get(n);
if (esn == null) {
String protoname = n.getPrototype();
// set the DbMapping of the node according to its prototype.
// this *should* be done on the objectmodel level, but isn't currently
// for embedded nodes since there's not enough type info at the objectmodel level
// for those nodes.
/* if ((protoname != null) && (protoname.length() > 0) &&
(n.getDbMapping() == null)) {
n.setDbMapping(app.getDbMapping(protoname));
} */
Scriptable op = null;
op = getValidPrototype(protoname);
// no prototype found for this node?
if (op == null) {
op = getValidPrototype("hopobject");
protoname = "hopobject";
}
esn = new HopObject(protoname);
esn.init(this, n);
esn.setPrototype(op);
wrappercache.put(n, esn);
}
return esn;
}
protected String postProcessHref(Object obj, String protoName, String basicHref) {
// check if the app.properties specify a href-function to post-process the
// basic href.
String hrefFunction = app.getProperty("hrefFunction", null);
if (hrefFunction != null) {
Object handler = obj;
String proto = protoName;
while (handler != null) {
if (hasFunction(proto, hrefFunction)) {
// get the currently active rhino engine and invoke the function
Context cx = Context.getCurrentContext();
RhinoEngine engine = (RhinoEngine) cx.getThreadLocal("engine");
Object result = null;
try {
result = engine.invoke(handler, hrefFunction,
new Object[] { basicHref }, false);
} catch (ScriptingException x) {
throw new RuntimeException("Error in hrefFunction: " + x);
}
if (result == null) {
throw new RuntimeException("hrefFunction " + hrefFunction +
" returned null");
}
basicHref = result.toString();
break;
}
handler = app.getParentElement(handler);
proto = app.getPrototypeName(handler);
}
}
// check if the app.properties specify a href-skin to post-process the
// basic href.
String hrefSkin = app.getProperty("hrefSkin", null);
if (hrefSkin != null) {
// we need to post-process the href with a skin for this application
// first, look in the object href was called on.
Skin skin = null;
Object handler = obj;
while (handler != null) {
Prototype proto = app.getPrototype(handler);
if (proto != null) {
skin = proto.getSkin(hrefSkin);
}
if (skin != null) {
break;
}
handler = app.getParentElement(handler);
}
if (skin != null) {
// get the currently active rhino engine and render the skin
Context cx = Context.getCurrentContext();
RhinoEngine engine = (RhinoEngine) cx.getThreadLocal("engine");
engine.getResponse().pushStringBuffer();
HashMap param = new HashMap();
param.put("path", basicHref);
skin.render(engine.getRequestEvaluator(), handler, param);
basicHref = engine.getResponse().popStringBuffer().trim();
}
}
return basicHref;
}
/////////////////////////////////////////////
// skin related methods
/////////////////////////////////////////////
protected static Object[] unwrapSkinpath(Object[] skinpath) {
if (skinpath != null) {
for (int i=0; i<skinpath.length; i++) {
if (skinpath[i] instanceof HopObject) {
skinpath[i] = ((HopObject) skinpath[i]).getNode();
} else if (skinpath[i] instanceof Wrapper) {
skinpath[i] = ((Wrapper) skinpath[i]).unwrap();
}
}
}
return skinpath;
}
protected static Map getSkinParam(Object paramobj) {
Map param = null;
if ((paramobj != null) && (paramobj != Undefined.instance)) {
param = new HashMap();
if (paramobj instanceof Scriptable) {
Scriptable sp = (Scriptable) paramobj;
Object[] ids = sp.getIds();
for (int i = 0; i < ids.length; i++) {
Object obj = sp.get(ids[i].toString(), sp);
if (obj instanceof Wrapper) {
param.put(ids[i], ((Wrapper) obj).unwrap());
} else if (obj != Undefined.instance) {
param.put(ids[i], obj);
}
}
}
}
return param;
}
////////////////////////////////////////////////
// private evaluation/compilation methods
////////////////////////////////////////////////
private synchronized void evaluate(Prototype prototype, TypeInfo info, Object code) {
if (code instanceof FunctionFile) {
FunctionFile funcfile = (FunctionFile) code;
File file = funcfile.getFile();
if (file != null) {
try {
FileReader fr = new FileReader(file);
updateEvaluator(prototype, info, fr, funcfile.getSourceName(), 1);
} catch (IOException iox) {
app.logEvent("Error updating function file: " + iox);
}
} else {
StringReader reader = new StringReader(funcfile.getContent());
updateEvaluator(prototype, info, reader, funcfile.getSourceName(), 1);
}
} else if (code instanceof ActionFile) {
ActionFile action = (ActionFile) code;
RhinoActionAdapter fa = new RhinoActionAdapter(action);
try {
updateEvaluator(prototype, info, new StringReader(fa.function),
action.getSourceName(), 0);
if (fa.functionAsString != null) {
// templates have an _as_string variant that needs to be compiled
updateEvaluator(prototype, info, new StringReader(fa.functionAsString),
action.getSourceName(), 0);
}
} catch (Exception esx) {
app.logEvent("Error parsing " + action + ": " + esx);
}
}
}
private synchronized void updateEvaluator(Prototype prototype, TypeInfo info,
Reader reader, String sourceName, int firstline) {
// System.err.println("UPDATE EVALUATOR: "+prototype+" - "+sourceName);
try {
// get the current context
Context cx = Context.getCurrentContext();
Scriptable op = info.objectPrototype;
// do the update, evaluating the file
cx.evaluateReader(op, reader, sourceName, firstline, null);
} catch (Exception e) {
app.logEvent("Error parsing file " + sourceName + ": " + e);
// also write to standard out unless we're logging to it anyway
if (!"console".equalsIgnoreCase(app.getProperty("logDir"))) {
System.err.println("Error parsing file " + sourceName + ": " + e);
}
// mark prototype as broken
if (info.error == null && e instanceof EcmaError) {
if ("global".equals(info.protoName)) {
globalError = (EcmaError) e;
} else {
info.error = (EcmaError) e;
}
wrappercache.clear();
}
// e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignore) {
// shouldn't happen
}
}
}
}
/**
* Return the global scope of this RhinoCore.
*/
public Scriptable getScope() {
return global;
}
/**
* TypeInfo helper class
*/
class TypeInfo {
// the object prototype for this type
ScriptableObject objectPrototype;
// timestamp of last update
long lastUpdate = 0;
// the prototype name
String protoName;
// a set of property values that were defined in last script compliation
Set compiledFunctions;
// a set of property keys that were present before first script compilation
final Set predefinedProperties;
EcmaError error;
public TypeInfo(ScriptableObject op, String name) {
objectPrototype = op;
protoName = name;
compiledFunctions = new HashSet(0);
// remember properties already defined on this object prototype
predefinedProperties = new HashSet();
Object[] keys = op.getAllIds();
for (int i = 0; i < keys.length; i++) {
predefinedProperties.add(keys[i].toString());
}
}
public String toString() {
return ("TypeInfo[" + protoName + "," + new Date(lastUpdate) + "]");
}
}
/**
* Object wrapper class
*/
class WrapMaker extends WrapFactory {
public Object wrap(Context cx, Scriptable scope, Object obj, Class staticType) {
// System.err.println ("Wrapping: "+obj);
if (obj instanceof INode) {
return getNodeWrapper((INode) obj);
}
if (obj instanceof SystemMap || obj instanceof WrappedMap) {
return new MapWrapper((Map) obj, RhinoCore.this);
}
if (obj != null && app.getPrototypeName(obj) != null) {
return getElementWrapper(obj);
}
return super.wrap(cx, scope, obj, staticType);
}
public Scriptable wrapNewObject(Context cx, Scriptable scope, Object obj) {
// System.err.println ("N-Wrapping: "+obj);
if (obj instanceof INode) {
return getNodeWrapper((INode) obj);
}
if (obj != null && app.getPrototypeName(obj) != null) {
return getElementWrapper(obj);
}
return super.wrapNewObject(cx, scope, obj);
}
}
class StringTrim extends BaseFunction {
public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String str = thisObj.toString();
return str.trim();
}
}
class DateFormat extends BaseFunction {
public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
Date date = new Date((long) ScriptRuntime.toNumber(thisObj));
SimpleDateFormat df = null;
if (args.length > 0 && args[0] != Undefined.instance) {
df = new SimpleDateFormat(args[0].toString());
} else {
df = new SimpleDateFormat();
}
return df.format(date);
}
}
class NumberFormat extends BaseFunction {
public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
DecimalFormat df = null;
if (args.length > 0 && args[0] != Undefined.instance) {
df = new DecimalFormat(args[0].toString());
} else {
df = new DecimalFormat("#,##0.00");
}
return df.format(ScriptRuntime.toNumber(thisObj)).toString();
}
}
}