001: /*
002: * Helma License Notice
003: *
004: * The contents of this file are subject to the Helma License
005: * Version 2.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://adele.helma.org/download/helma/license.txt
008: *
009: * Copyright 1998-2003 Helma Software. All Rights Reserved.
010: *
011: * $RCSfile$
012: * $Author: root $
013: * $Revision: 8604 $
014: * $Date: 2007-09-28 15:16:38 +0200 (Fre, 28 Sep 2007) $
015: */
016:
017: package helma.scripting.rhino;
018:
019: import helma.doc.DocApplication;
020: import helma.extensions.ConfigurationException;
021: import helma.extensions.HelmaExtension;
022: import helma.framework.*;
023: import helma.framework.repository.Resource;
024: import helma.framework.core.*;
025: import helma.main.Server;
026: import helma.objectmodel.*;
027: import helma.objectmodel.db.DbMapping;
028: import helma.objectmodel.db.Relation;
029: import helma.scripting.*;
030: import helma.scripting.rhino.debug.Tracer;
031: import helma.util.StringUtils;
032: import org.mozilla.javascript.*;
033: import org.mozilla.javascript.serialize.ScriptableOutputStream;
034: import org.mozilla.javascript.serialize.ScriptableInputStream;
035:
036: import java.util.*;
037: import java.io.*;
038: import java.lang.ref.WeakReference;
039:
040: /**
041: * This is the implementation of ScriptingEnvironment for the Mozilla Rhino EcmaScript interpreter.
042: */
043: public class RhinoEngine implements ScriptingEngine {
044: // map for Application to RhinoCore binding
045: static final Map coreMap = new WeakHashMap();
046:
047: // the application we're running in
048: public Application app;
049:
050: // The Rhino context
051: Context context;
052:
053: // the per-thread global object
054: GlobalObject global;
055:
056: // the request evaluator instance owning this fesi evaluator
057: RequestEvaluator reval;
058:
059: // the rhino core
060: RhinoCore core;
061:
062: // the global vars set by extensions
063: HashMap extensionGlobals;
064:
065: // the thread currently running this engine
066: volatile Thread thread;
067:
068: // thread local engine registry
069: static ThreadLocal engines = new ThreadLocal();
070:
071: // the introspector that provides documentation for this application
072: DocApplication doc = null;
073:
074: /**
075: * Zero argument constructor.
076: */
077: public RhinoEngine() {
078: // nothing to do
079: }
080:
081: /**
082: * Init the scripting engine with an application and a request evaluator
083: */
084: public synchronized void init(Application app,
085: RequestEvaluator reval) {
086: this .app = app;
087: this .reval = reval;
088: initRhinoCore(app);
089:
090: context = core.contextFactory.enter();
091:
092: try {
093: extensionGlobals = new HashMap();
094:
095: if (Server.getServer() != null) {
096: Vector extVec = Server.getServer().getExtensions();
097:
098: for (int i = 0; i < extVec.size(); i++) {
099: HelmaExtension ext = (HelmaExtension) extVec.get(i);
100:
101: try {
102: HashMap tmpGlobals = ext.initScripting(app,
103: this );
104:
105: if (tmpGlobals != null) {
106: extensionGlobals.putAll(tmpGlobals);
107: }
108: } catch (ConfigurationException e) {
109: app.logError("Couldn't initialize extension "
110: + ext.getName(), e);
111: }
112: }
113: }
114:
115: } catch (Exception e) {
116: app.logError("Cannot initialize interpreter", e);
117: throw new RuntimeException(e.getMessage(), e);
118: } finally {
119: core.contextFactory.exit();
120: }
121: }
122:
123: /**
124: * Return the RhinoEngine associated with the current thread, or null.
125: * @return the RhinoEngine assocated with the current thread
126: */
127: public static RhinoEngine getRhinoEngine() {
128: return (RhinoEngine) engines.get();
129: }
130:
131: /**
132: * Initialize the RhinoCore instance for this engine and application.
133: * @param app the application we belong to
134: */
135: private synchronized void initRhinoCore(Application app) {
136: synchronized (coreMap) {
137: WeakReference ref = (WeakReference) coreMap.get(app);
138: if (ref != null) {
139: core = (RhinoCore) ref.get();
140: }
141:
142: if (core == null) {
143: core = new RhinoCore(app);
144: core.initialize();
145: coreMap.put(app, new WeakReference(core));
146: }
147: }
148: }
149:
150: /**
151: * This method is called before an execution context is entered to let the
152: * engine know it should update its prototype information.
153: */
154: public synchronized void updatePrototypes() throws IOException {
155: // remember the current thread as our thread - we do this here so
156: // the thread is already set when the RequestEvaluator calls
157: // Application.getDataRoot(), which may result in a function invocation
158: // (chicken and egg problem, kind of)
159: thread = Thread.currentThread();
160: global = new GlobalObject(core, app, true);
161: context = core.contextFactory.enter();
162:
163: if (core.hasTracer) {
164: context.setDebugger(new Tracer(getResponse()), null);
165: }
166:
167: // register the engine with the current thread
168: engines.set(this );
169: // update prototypes
170: core.updatePrototypes();
171: }
172:
173: /**
174: * This method is called when an execution context for a request
175: * evaluation is entered. The globals parameter contains the global values
176: * to be applied during this execution context.
177: */
178: public synchronized void enterContext(Map globals)
179: throws ScriptingException {
180: // remember the current thread as our thread
181: thread = Thread.currentThread();
182:
183: // set globals on the global object
184: // add globals from extensions
185: globals.putAll(extensionGlobals);
186: // loop through global vars and set them
187: for (Iterator i = globals.keySet().iterator(); i.hasNext();) {
188: String k = (String) i.next();
189: Object v = globals.get(k);
190: Scriptable scriptable;
191:
192: // create a special wrapper for the path object.
193: // other objects are wrapped in the default way.
194: if (v == null) {
195: continue;
196: } else if (v instanceof RequestPath) {
197: scriptable = new PathWrapper((RequestPath) v, core);
198: scriptable.setPrototype(core.pathProto);
199: } else {
200: scriptable = Context.toObject(v, global);
201: }
202:
203: global.put(k, global, scriptable);
204: }
205: }
206:
207: /**
208: * This method is called to let the scripting engine know that the current
209: * execution context has terminated.
210: */
211: public synchronized void exitContext() {
212: // unregister the engine threadlocal
213: engines.set(null);
214: core.contextFactory.exit();
215: thread = null;
216: global = null;
217: }
218:
219: /**
220: * Invoke a function on some object, using the given arguments and global vars.
221: * XML-RPC calls require special input and output parameter conversion.
222: *
223: * @param thisObject the object to invoke the function on, or null for
224: * global functions
225: * @param function the function or name of the function to be invoked
226: * @param args array of argument objects
227: * @param argsWrapMode indicated the way to process the arguments. Must be
228: * one of <code>ARGS_WRAP_NONE</code>,
229: * <code>ARGS_WRAP_DEFAULT</code>,
230: * <code>ARGS_WRAP_XMLRPC</code>
231: * @param resolve indicates whether functionName may contain an object path
232: * or just the plain function name
233: * @return the return value of the function
234: * @throws ScriptingException to indicate something went wrong
235: * with the invocation
236: */
237: public Object invoke(Object this Object, Object function,
238: Object[] args, int argsWrapMode, boolean resolve)
239: throws ScriptingException {
240: if (function == null) {
241: throw new IllegalArgumentException(
242: "Function argument must not be null");
243: }
244: if (args == null) {
245: throw new IllegalArgumentException(
246: "Arguments array must not be null");
247: }
248: try {
249: Scriptable obj = this Object == null ? global : Context
250: .toObject(this Object, global);
251: Function func;
252: if (function instanceof String) {
253: String funcName = (String) function;
254: // if function name should be resolved interpret it as member expression,
255: // otherwise replace dots with underscores.
256: if (resolve) {
257: if (funcName.indexOf('.') > 0) {
258: String[] path = StringUtils
259: .split(funcName, ".");
260: for (int i = 0; i < path.length - 1; i++) {
261: Object propValue = ScriptableObject
262: .getProperty(obj, path[i]);
263: if (propValue instanceof Scriptable) {
264: obj = (Scriptable) propValue;
265: } else {
266: throw new RuntimeException(
267: "Can't resolve function name "
268: + funcName + " in "
269: + this Object);
270: }
271: }
272: funcName = path[path.length - 1];
273: }
274: } else {
275: funcName = funcName.replace('.', '_');
276: }
277: Object funcvalue = ScriptableObject.getProperty(obj,
278: funcName);
279:
280: if (!(funcvalue instanceof Function))
281: return null;
282: func = (Function) funcvalue;
283:
284: } else {
285: if (function instanceof Wrapper)
286: function = ((Wrapper) function).unwrap();
287: if (!(function instanceof Function))
288: throw new IllegalArgumentException(
289: "Not a function or function name: "
290: + function);
291: func = (Function) function;
292: }
293:
294: for (int i = 0; i < args.length; i++) {
295: switch (argsWrapMode) {
296: case ARGS_WRAP_DEFAULT:
297: // convert java objects to JavaScript
298: if (args[i] != null) {
299: args[i] = Context.javaToJS(args[i], global);
300: }
301: break;
302: case ARGS_WRAP_XMLRPC:
303: // XML-RPC requires special argument conversion
304: args[i] = core.processXmlRpcArgument(args[i]);
305: break;
306: }
307: }
308:
309: // use Context.call() in order to set the context's factory
310: Object retval = Context.call(core.contextFactory, func,
311: global, obj, args);
312:
313: if (retval instanceof Wrapper) {
314: retval = ((Wrapper) retval).unwrap();
315: }
316:
317: if ((retval == null) || (retval == Undefined.instance)) {
318: return null;
319: } else if (argsWrapMode == ARGS_WRAP_XMLRPC) {
320: return core.processXmlRpcResponse(retval);
321: } else {
322: return retval;
323: }
324: } catch (RedirectException redirect) {
325: throw redirect;
326: } catch (TimeoutException timeout) {
327: throw timeout;
328: } catch (ConcurrencyException concur) {
329: throw concur;
330: } catch (Exception x) {
331: // has the request timed out? If so, throw TimeoutException
332: if (thread != Thread.currentThread()) {
333: throw new TimeoutException();
334: }
335:
336: if (x instanceof WrappedException) {
337: // wrapped java excepiton
338: Throwable wrapped = ((WrappedException) x)
339: .getWrappedException();
340: // rethrow if this is a wrapped concurrency or redirect exception
341: if (wrapped instanceof ConcurrencyException) {
342: throw (ConcurrencyException) wrapped;
343: } else if (wrapped instanceof RedirectException) {
344: throw (RedirectException) wrapped;
345: }
346: }
347: // create and throw a ScriptingException with the right message
348: String msg = x.getMessage();
349: throw new ScriptingException(msg, x);
350: }
351: }
352:
353: /**
354: * Let the evaluator know that the current evaluation has been
355: * aborted.
356: */
357: public void abort() {
358: // current request has been aborted.
359: Thread t = thread;
360: // set thread to null
361: thread = null;
362: if (t != null && t.isAlive()) {
363: t.interrupt();
364: try {
365: t.join(1000);
366: } catch (InterruptedException ir) {
367: // interrupted by other thread
368: }
369: }
370: }
371:
372: /**
373: * Check if an object has a function property (public method if it
374: * is a java object) with that name.
375: */
376: public boolean hasFunction(Object obj, String fname, boolean resolve) {
377: if (resolve) {
378: if (fname.indexOf('.') > 0) {
379: Scriptable op = obj == null ? global : Context
380: .toObject(obj, global);
381: String[] path = StringUtils.split(fname, ".");
382: for (int i = 0; i < path.length; i++) {
383: Object value = ScriptableObject.getProperty(op,
384: path[i]);
385: if (value instanceof Scriptable) {
386: op = (Scriptable) value;
387: } else {
388: return false;
389: }
390: }
391: return (op instanceof Function);
392: }
393: } else {
394: // Convert '.' to '_' in function name
395: fname = fname.replace('.', '_');
396: }
397:
398: // Treat HopObjects separately - otherwise we risk to fetch database
399: // references/child objects just to check for function properties.
400: if (obj instanceof INode) {
401: String protoname = ((INode) obj).getPrototype();
402: if (protoname != null && core.hasFunction(protoname, fname))
403: return true;
404: }
405:
406: Scriptable op = obj == null ? global : Context.toObject(obj,
407: global);
408: return ScriptableObject.getProperty(op, fname) instanceof Callable;
409: }
410:
411: /**
412: * Check if an object has a value property defined with that name.
413: */
414: public boolean hasProperty(Object obj, String propname) {
415: if (obj == null || propname == null) {
416: return false;
417: } else if (obj instanceof Map) {
418: return ((Map) obj).containsKey(propname);
419: }
420:
421: String prototypeName = app.getPrototypeName(obj);
422:
423: if ("user".equalsIgnoreCase(prototypeName)
424: && "password".equalsIgnoreCase(propname)) {
425: return false;
426: }
427:
428: // if this is a HopObject, check if the property is defined
429: // in the type.properties db-mapping.
430: if (obj instanceof INode
431: && !"hopobject".equalsIgnoreCase(prototypeName)) {
432: DbMapping dbm = app.getDbMapping(prototypeName);
433: if (dbm != null) {
434: Relation rel = dbm.propertyToRelation(propname);
435: if (rel != null
436: && (rel.isPrimitive() || rel.isCollection()))
437: return true;
438: }
439: }
440: Scriptable wrapped = Context.toObject(obj, global);
441: return wrapped.has(propname, wrapped);
442: }
443:
444: /**
445: * Check if an object has a defined property (public field if it
446: * is a java object) with that name.
447: */
448: public Object getProperty(Object obj, String propname) {
449: if (obj == null || propname == null) {
450: return null;
451: } else if (obj instanceof Map) {
452: Object prop = ((Map) obj).get(propname);
453: // Do not return functions as properties as this
454: // is a potential security problem
455: return (prop instanceof Function) ? null : prop;
456: }
457:
458: // use Rhino wrappers and methods to get property
459: Scriptable so = Context.toObject(obj, global);
460:
461: try {
462: Object prop = so.get(propname, so);
463:
464: if (prop == null || prop == Undefined.instance
465: || prop == ScriptableObject.NOT_FOUND) {
466: return null;
467: } else if (prop instanceof Wrapper) {
468: return ((Wrapper) prop).unwrap();
469: } else {
470: // Do not return functions as properties as this
471: // is a potential security problem
472: return (prop instanceof Function) ? null : prop;
473: }
474: } catch (Exception esx) {
475: app.logError("Error getting property " + propname + ": "
476: + esx);
477: return null;
478: }
479: }
480:
481: /**
482: * Determine if the given object is mapped to a type of the scripting engine
483: * @param obj an object
484: * @return true if the object is mapped to a type
485: */
486: public boolean isTypedObject(Object obj) {
487: if (obj instanceof Wrapper)
488: obj = ((Wrapper) obj).unwrap();
489: if (obj == null || obj instanceof Map
490: || obj instanceof NativeObject)
491: return false;
492: if (obj instanceof IPathElement) {
493: String protoName = ((IPathElement) obj).getPrototype();
494: return protoName != null
495: && !"hopobject".equalsIgnoreCase(protoName);
496: }
497: // assume java object is typed
498: return true;
499: }
500:
501: /**
502: * Return a string representation for the given object
503: * @param obj an object
504: * @return a string representing the object
505: */
506: public String toString(Object obj) {
507: // not all Rhino types convert to a string as expected
508: // when calling toString() - try to do better by using
509: // Rhino's ScriptRuntime.toString(). Note that this
510: // assumes that people always use this method to get
511: // a string representation of the object - which is
512: // currently the case since it's only used in Skin rendering.
513: try {
514: return ScriptRuntime.toString(obj);
515: } catch (Exception x) {
516: // just return original property object
517: }
518: return obj.toString();
519: }
520:
521: /**
522: * Get an introspector to this engine.
523: */
524: public DocApplication getDoc() {
525: if (doc == null) {
526: try {
527: doc = new DocApplication(app);
528: doc.readApplication();
529: } catch (IOException x) {
530: throw new RuntimeException(x.toString(), x);
531: }
532: }
533: return doc;
534: }
535:
536: /**
537: * Provide object serialization for this engine's scripted objects. If no special
538: * provisions are required, this method should just wrap the stream with an
539: * ObjectOutputStream and write the object.
540: *
541: * @param obj the object to serialize
542: * @param out the stream to write to
543: * @throws java.io.IOException
544: */
545: public void serialize(Object obj, OutputStream out)
546: throws IOException {
547: core.contextFactory.enter();
548: try {
549: // use a special ScriptableOutputStream that unwraps Wrappers
550: ScriptableOutputStream sout = new ScriptableOutputStream(
551: out, core.global) {
552: protected Object replaceObject(Object obj)
553: throws IOException {
554: if (obj instanceof Wrapper)
555: obj = ((Wrapper) obj).unwrap();
556: return super .replaceObject(obj);
557: }
558: };
559: sout.writeObject(obj);
560: sout.flush();
561: } finally {
562: core.contextFactory.exit();
563: }
564: }
565:
566: /**
567: * Provide object deserialization for this engine's scripted objects. If no special
568: * provisions are required, this method should just wrap the stream with an
569: * ObjectIntputStream and read the object.
570: *
571: * @param in the stream to read from
572: * @return the deserialized object
573: * @throws java.io.IOException
574: */
575: public Object deserialize(InputStream in) throws IOException,
576: ClassNotFoundException {
577: core.contextFactory.enter();
578: try {
579: ObjectInputStream sin = new ScriptableInputStream(in,
580: core.global);
581: return sin.readObject();
582: } finally {
583: core.contextFactory.exit();
584: }
585: }
586:
587: /**
588: * Add a code resource to a given prototype by immediately compiling and evaluating it.
589: *
590: * @param typename the type this resource belongs to
591: * @param resource a code resource
592: */
593: public void injectCodeResource(String typename, Resource resource) {
594: // we activate recording on thread scope to make it forward
595: // property puts to the shared scope (bug 504)
596: if (global != null)
597: global.startRecording();
598: try {
599: core.injectCodeResource(typename, resource);
600: } finally {
601: if (global != null)
602: global.stopRecording();
603: }
604: }
605:
606: /**
607: * Return the application we're running in
608: */
609: public Application getApplication() {
610: return app;
611: }
612:
613: /**
614: * Return the RequestEvaluator owning and driving this FESI evaluator.
615: */
616: public RequestEvaluator getRequestEvaluator() {
617: return reval;
618: }
619:
620: /**
621: * Return the Response object of the current evaluation context.
622: * Proxy method to RequestEvaluator.
623: */
624: public ResponseTrans getResponse() {
625: return reval.getResponse();
626: }
627:
628: /**
629: * Return the Request object of the current evaluation context.
630: * Proxy method to RequestEvaluator.
631: */
632: public RequestTrans getRequest() {
633: return reval.getRequest();
634: }
635:
636: /**
637: * Return the RhinoCore object for the application this engine belongs to.
638: *
639: * @return this engine's RhinoCore instance
640: */
641: public RhinoCore getCore() {
642: return core;
643: }
644:
645: /**
646: * Try to get a skin from the parameter object.
647: */
648: public Skin toSkin(Object skinobj, String protoName)
649: throws IOException {
650: if (skinobj == null) {
651: return null;
652: } else if (skinobj instanceof Wrapper) {
653: skinobj = ((Wrapper) skinobj).unwrap();
654: }
655:
656: if (skinobj instanceof Skin) {
657: return (Skin) skinobj;
658: } else {
659: return getSkin(protoName, skinobj.toString());
660: }
661: }
662:
663: /**
664: * Get a skin for the given prototype and skin name. This method considers the
665: * skinpath set in the current response object and does per-response skin
666: * caching.
667: */
668: public Skin getSkin(String protoName, String skinName)
669: throws IOException {
670: Skin skin;
671: ResponseTrans res = getResponse();
672: if (skinName.startsWith("#")) {
673: // evaluate relative subskin name against currently rendering skin
674: skin = res.getActiveSkin();
675: return skin == null ? null : skin.getSubskin(skinName
676: .substring(1));
677: }
678:
679: SkinKey key = new SkinKey(protoName, skinName);
680: skin = res.getCachedSkin(key);
681:
682: if (skin == null) {
683: // retrieve res.skinpath, an array of objects that tell us where to look for skins
684: // (strings for directory names and INodes for internal, db-stored skinsets)
685: Object[] skinpath = res.getSkinpath();
686: RhinoCore.unwrapSkinpath(skinpath);
687: skin = app.getSkin(protoName, skinName, skinpath);
688: res.cacheSkin(key, skin);
689: }
690: return skin;
691: }
692:
693: }
|