2002/03/21 01:19:34
[org.ibex.core.git] / src / org / xwt / util / JSObject.java
diff --git a/src/org/xwt/util/JSObject.java b/src/org/xwt/util/JSObject.java
new file mode 100644 (file)
index 0000000..81f36ea
--- /dev/null
@@ -0,0 +1,194 @@
+// Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL]
+package org.xwt.util;
+
+import org.mozilla.javascript.*;
+import java.net.*;
+import java.io.*;
+import java.util.*;
+import org.xwt.*;
+
+/** Simple implementation of a JavaScript host object.
+ *
+ *  If privateVars is set, then any "var" declaration issued on this
+ *  scope will create a variable visible only to scripts from the same
+ *  directory (package) as the script that made the declaration.
+ *
+ *  This is implemented by making JSObject.Top the ultimate
+ *  parentScope of all privateVars-style JSObjects. Puts to JSObjects
+ *  which are var-decls will not climb the scope chain, and will be
+ *  put directly to the JSObject. Puts which are <i>not</i> var-decls
+ *  will climb the scope chain and will be put to Top, which will then
+ *  re-put the value to the JSObject as a global.
+ */
+public class JSObject implements Scriptable {
+
+    // Static Data ///////////////////////////////////////////////////////////////////////    
+
+    /** helper; retrieves the 'source code filename' (usually the nodeName) of the currently-executing function in this thread */
+    public static String getCurrentFunctionSourceName() {
+        Function cf = Context.getCurrentContext().currentFunction;
+        if (cf == null) return null;
+        return (cf instanceof InterpretedFunction) ?
+            ((InterpretedFunction)cf).getSourceName() : ((InterpretedScript)cf).getSourceName();
+    }
+
+    /** cache for nodeNameToPackageName(); prevents creation of zillions of String objects */
+    private static Hash nodeNameToPackageNameHash = new Hash(1000, 2);
+
+    /** converts a nodeName (source code filename) into the 'package name' (resource name of its directory) */
+    private static String nodeNameToPackageName(String nodeName) {
+        if (nodeName == null) return null;
+        String ret = (String)nodeNameToPackageNameHash.get(nodeName);
+        if (ret != null) return ret;
+        ret = nodeName;
+        for(int i=0; i<ret.length() - 1; i++)
+            if (ret.charAt(i) == '.' && (ret.charAt(i + 1) == '_' || Character.isDigit(ret.charAt(i + 1)))) {
+                ret = ret.substring(0, i);
+                break;
+            }
+        if (ret.lastIndexOf('.') != -1)
+            ret = ret.substring(0, ret.lastIndexOf('.'));
+        ret = ret.intern();
+        nodeNameToPackageNameHash.put(nodeName, ret);
+        return ret;
+    }
+    
+    /** this holds a scope which has been initialized with the standard ECMA objects */
+    public static Scriptable defaultObjects = Context.enter().initStandardObjects(null);
+
+    // Instance Data ///////////////////////////////////////////////////////////////////////    
+
+    /** The properties that holds our scope */
+    protected Hash properties = null;
+    
+    /** If true, put()s and delete()s are prohibited */
+    boolean sealed = false;
+
+    /** If true, variables declared with 'var' become visible only to scripts from the same
+     *  directory/package as the one which created the variable
+     */
+    boolean privateVars = false;
+
+    public JSObject() { }
+    public JSObject(boolean privateVars) { this.privateVars = privateVars; }
+
+    public Object get(String name, Scriptable start) {
+        if (name == null || name.equals("") || properties == null) return null;
+
+        Object ret = properties.get(name, nodeNameToPackageName(getCurrentFunctionSourceName()));
+        if (ret == NOT_FOUND) return null;
+        if (ret != null) return ret;
+        return properties.get(name, null);
+    }
+
+    /** returns the package-private variable <tt>name</tt> associated with <tt>nodeName</tt> */
+    public Object getPrivately(String name, String nodeName) {
+        return properties == null ? null : properties.get(name, nodeNameToPackageName(nodeName));
+    }
+
+    /** puts a property as a private variable, accessible only to scripts in the source code file <tt>nodeName</tt> */
+    public void putPrivately(String name, Object value, String nodeName) {
+        if (!privateVars) {
+            putGlobally(name, null, value);
+            return;
+        }
+        if (properties == null) properties = new Hash();
+
+        // we use NOT_FOUND to mark a variable which exists as a script-local, yet has a value of null
+        if (value == null) value = NOT_FOUND;
+        properties.put(name, nodeNameToPackageName(nodeName), value);
+    }
+
+    /** forces a property to be put globally -- accessible to all scripts */
+    public void putGlobally(String name, Scriptable start, Object value) {
+        if (name == null || name.equals("")) return;
+        if (properties == null) properties = new Hash();
+        properties.put(name, null, value);
+    }
+
+    /** if a put is made directly to us (rather than cascading up to Top), it must be a var-declaration */
+    public void put(String name, Scriptable start, Object value) {
+        if (sealed) return;
+        if (name == null || name.equals("")) return;
+        putPrivately(name, value, getCurrentFunctionSourceName());
+    }
+    
+    /** if privateVars is enabled, we always return false, to see if the put propagates up to Top */
+    public boolean has(String name, Scriptable start) {
+        if (!privateVars) return properties != null && properties.get(name) != null;
+        Top.lastByThread.put(Thread.currentThread(), this);
+        return false;
+    }
+    
+    public Object[] getIds() {
+        if (properties == null) return new Object[] { };
+        else return properties.keys();
+    }
+    
+    /** Parent scope of all JSObjects; we use the has(String) method on this object to determine if a put represents a var-declaration or a plain 'ol put */
+    private static class Top implements Scriptable {
+        
+        /** Last JSObject to perform a has(), keyed by thread.
+         *  Assumes that every Top.has() is immediately preceded by a JSObject.has() in the same thread
+         */
+        public static Hash lastByThread = new Hash();
+        
+        public static Top singleton = new Top();
+        private Top() { }
+        
+        public boolean has(String name, Scriptable start) { return true; }
+        public void put(String name, Scriptable start, Object value) {
+            JSObject last = (JSObject)lastByThread.get(Thread.currentThread());
+            if (last.properties != null && last.properties.get(name, nodeNameToPackageName(getCurrentFunctionSourceName())) != null)
+                last.put(name, start, value);
+            else last.putGlobally(name, start, value);
+        }
+
+        public Object get(String name, Scriptable start) {
+
+            // hack since Rhino needs to be able to grab these functions to create new objects
+            if (name.equals("Object")) return JSObject.defaultObjects.get("Object", null);
+            if (name.equals("Array")) return JSObject.defaultObjects.get("Array", null);
+            if (name.equals("Function")) return JSObject.defaultObjects.get("Function", null);
+            if (name.equals("TypeError")) return JSObject.defaultObjects.get("TypeError", null);
+            return ((JSObject)lastByThread.get(Thread.currentThread())).get(name, start);
+        }
+
+        // Trivial methods
+        public String getClassName() { return "Top"; }
+        public Scriptable construct(Context cx, Scriptable scope, java.lang.Object[] args) { return null; }
+        public void delete(String name) { }
+        public Scriptable getParentScope() { return null; }
+        public void setParentScope(Scriptable p) { }
+        public boolean hasInstance(Scriptable value) { return false; }
+        public Scriptable getPrototype() { return null; }
+        public void setPrototype(Scriptable p) { }
+        public void delete(int i) { }
+        public Object getDefaultValue(Class hint) { return "Top"; }
+        public void put(int i, Scriptable start, Object value) { }
+        public Object get(int i, Scriptable start) { return null; }
+        public boolean has(int i, Scriptable start) { return false; }
+        public Object[] getIds() { return new Object[] { }; }
+
+    }
+
+
+    // Trivial Methods ///////////////////////////////////////////////////////////
+
+    public void setSeal(boolean doseal) { sealed = doseal; }
+    public void flush() { if (properties != null) properties.clear(); }
+    public void delete(String name) { if (Log.on) Log.log(this, "JSObject.delete() should not be reachable"); }
+    public void delete(int i) { if (Log.on) Log.log(this, "JSObject.delete() should not be reachable"); }
+    public Scriptable getParentScope() { return privateVars ? Top.singleton : null; }
+    public void setParentScope(Scriptable p) { }
+    public boolean hasInstance(Scriptable value) { return false; }
+    public Scriptable getPrototype() { return null; }
+    public void setPrototype(Scriptable p) { }
+    public String getClassName() { return this.getClass().getName(); }
+    public Object getDefaultValue(Class hint) { return toString(); }
+    public void put(int i, Scriptable start, Object value) { }
+    public Object get(int i, Scriptable start) { return null; }
+    public boolean has(int i, Scriptable start) { return false; }
+
+}
+