helma/src/FESI/Data/ESObject.java

785 lines
29 KiB
Java
Raw Blame History

// ESObject.java
// FESI Copyright (c) Jean-Marc Lugrin, 1999
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package FESI.Data;
import FESI.Exceptions.*;
import FESI.Interpreter.Evaluator;
import FESI.Interpreter.FesiHashtable;
import FESI.Interpreter.ScopeChain;
import java.util.Enumeration;
public abstract class ESObject extends ESValue {
/** Contains the properties of this object */
protected FesiHashtable properties;
/** The evaluator owning this object */
protected Evaluator evaluator;
/**
* The prototype of this object ([[prototype]] in the standard,
* not the "prototype" property of functions!)
*/
private ESObject prototype = null;
// Prepare common names and their hash value
static private final String TOSTRINGstring = ("toString").intern();
static private final int TOSTRINGhash = TOSTRINGstring.hashCode();
static private final String VALUEOFstring = ("valueOf").intern();
static private final int VALUEOFhash = VALUEOFstring.hashCode();
/**
* Create an object with a specific prototype (which may be null)
* in the context of a specific evaluator (which may not be null)
* Uses the default hashTable size.
*
* @param prototype The prototype ESObject - may be null
* @param evaluator The evaluator - must not be null
*/
protected ESObject(ESObject prototype, Evaluator evaluator) {
this.prototype = prototype;
this.properties = new FesiHashtable();
this.evaluator = evaluator; // It will crash somewhere if null...
}
/**
* Create an object with a specific prototype (which may be null)
* in the context of a specific evaluator (which may not be null)
* Uses the specified hashTable size, which should be a prime.
* size is usefull for very small (arguments) or very large objects.
*
* @param prototype The prototype ESObject - may be null
* @param evaluator The evaluator - must not be null
*/
protected ESObject(ESObject prototype, Evaluator evaluator, int initialSize) {
this.prototype = prototype;
this.properties = new FesiHashtable(initialSize);
this.evaluator = evaluator; // It will crash somewhere if null...
}
/**
* Get the evaluator for this object
*
* @return the evaluator
*/
public final Evaluator getEvaluator() {
return evaluator;
}
/**
* All objects and thir subclasses are non primitive
*
* @return false
*/
public final boolean isPrimitive() {
return false;
}
/**
* Implements the [[prototype]] property (see 8.6.2)
*
* @return The prototype object or null
*/
public ESObject getPrototype() {
return prototype;
}
/**
* Allow the prototype to be set, added 2001-04-05 by Hannes Walln<6C>fer
*
* @param prototype The new prototype object
*/
public void setPrototype(ESObject prototype) {
this.prototype = prototype;
}
/**
* Return the name of the class of objects ([[class]]), as used in the default toString
* method of objects (15.2.4.2)
*
* @return the [[Class]] property of this object
*/
public String getESClassName() {
return "Object";
}
/**
* Return a code indicating the type of the object for the implementation
* of the "==" operator.
*
* @return A type code
*/
public int getTypeOf() {
return EStypeObject;
}
/**
* Either return the property value of the specified property
* in the current object, or lookup the next object in the scope chain
* if there is one. If there is nones, generate an error message.
* <P>This routine must be overriden by subclass which change the
* implementation of getProperty.
*
* @param propertyName The property to look for
* @param previousScope The previous scope or null
* @param hash The hashCode of propertyName
* @return The value of the specified variable
* @exception EcmaScriptException if not found in any scope
*/
public ESValue getPropertyInScope(String propertyName,
ScopeChain previousScope,
int hash)
throws EcmaScriptException {
ESValue value = (ESValue) properties.get(propertyName, hash);
if (value == null) {
if (previousScope == null) {
// Return null for undefined global variables.
// throw new EcmaScriptException("global variable '" + propertyName + "' does not have a value");
value = ESNull.theNull;
} else {
value = previousScope.getValue(propertyName, hash);
}
}
return value;
}
/**
* Get the property by name (see 8.6.2.1) propagating to
* the prototype if required
*
* @param propertyName The string naming the property
* @param hash The hashCode of propertyName
* @return The property or <em>undefined</em>
* @exception EcmaScriptException Error in host objects ?
*/
public ESValue getProperty(String propertyName, int hash)
throws EcmaScriptException {
ESValue value = (ESValue) properties.get(propertyName, hash);
if (value == null) {
if (prototype == null) {
value = ESUndefined.theUndefined;
} else {
value = prototype.getProperty(propertyName, hash);
}
}
return value;
}
/**
* Get the property by index value. By default the index is
* converted to a string, but this can be optimized for arrays.
* <P>This is not the same as the indexed properties of the first
* version of JavaScript and does not allow to access named
* properties other than the property using the integer string
* representation as a name.
*
* @param index The property name as an integer.
* @return The property or <em>undefined</em>
* @exception EcmaScriptException Error in host objects ?
*/
public ESValue getProperty(int index)
throws EcmaScriptException {
String iString = Integer.toString(index);
return getProperty(iString, iString.hashCode());
}
public boolean hasProperty(String propertyName, int hash)
throws EcmaScriptException {
boolean found = properties.containsKey(propertyName, hash);
if (!found && prototype != null) {
found = prototype.hasProperty(propertyName, hash);
}
return found;
}
public boolean isHiddenProperty(String propertyName, int hash) {
return properties.isHidden(propertyName, hash);
}
/**
* Indicates that the getProperties return an enumerator to the
* index rather rather than to the value index (see ESWrapper).
*
* @return false
*/
public boolean isDirectEnumerator() {
return false;
}
/**
* Returns an enumerator for the key elements of this object,
* that is all is enumerable properties and the (non hidden)
* ones of its prototype, etc... As used for the for in
* statement.
*
* @return the enumerator
*/
public Enumeration getProperties() {
return new Enumeration() {
Enumeration props = properties.keys();
String currentKey = null;
int currentHash = 0;
boolean inside = false;
public boolean hasMoreElements() {
if (currentKey != null) return true;
while (props.hasMoreElements()) {
currentKey = (String) props.nextElement();
currentHash = currentKey.hashCode();
if (inside) {
if (properties.containsKey(currentKey, currentHash)) continue;
} else {
if (isHiddenProperty(currentKey, currentHash)) continue;
}
return true;
}
if (!inside && prototype != null) {
inside = true;
props = prototype.getProperties();
while (props.hasMoreElements()) {
currentKey = (String) props.nextElement();
currentHash = currentKey.hashCode();
if (properties.containsKey(currentKey, currentHash)) continue;
return true;
}
}
return false;
}
public Object nextElement() {
if (hasMoreElements()) {
String key = currentKey;
currentKey = null;
return key;
} else {
throw new java.util.NoSuchElementException();
}
}
};
}
/**
* Get all properties (including hidden ones), for the command
* @listall of the interpreter. Include the visible properties of the
* prototype (that is the one added by the user) but not the
* hidden ones of the prototype (otherwise this would list
* all functions for any object).
*
* @return An enumeration of all properties (visible and hidden).
*/
public Enumeration getAllProperties() {
return new Enumeration() {
String [] specialProperties = getSpecialPropertyNames();
int specialEnumerator = 0;
Enumeration props = properties.keys(); // all of object properties
String currentKey = null;
int currentHash = 0;
boolean inside = false; // true when examing prototypes properties
public boolean hasMoreElements() {
// OK if we already checked for a property and one exists
if (currentKey != null) return true;
// Loop on special properties first
if (specialEnumerator < specialProperties.length) {
currentKey = specialProperties[specialEnumerator];
currentHash = currentKey.hashCode();
specialEnumerator++;
return true;
}
// loop on standard or prototype properties
while (props.hasMoreElements()) {
currentKey = (String) props.nextElement();
currentHash = currentKey.hashCode();
if (inside) {
if (properties.containsKey(currentKey, currentHash)) continue;
// SHOULD CHECK IF NOT IN SPECIAL
}
return true;
}
// If prototype properties have not yet been examined, look for them
if (!inside && prototype != null) {
inside = true;
props = prototype.getProperties();
while (props.hasMoreElements()) {
currentKey = (String) props.nextElement();
currentHash = currentKey.hashCode();
if (properties.containsKey(currentKey, currentHash)) continue;
return true;
}
}
return false;
}
public Object nextElement() {
if (hasMoreElements()) {
String key = currentKey;
currentKey = null;
return key;
} else {
throw new java.util.NoSuchElementException();
}
}
};
}
/**
* Put the property by name (see 8.6.2.2), ignoring if
* read only (integrate functionality of canPut) and
* creating it if needed and possible.
* <P>The routine implement the functionality of the canPut attribute.
*
* @param propertyName The string naming the property
* @param propertyValue The value to put
* @exception EcmaScriptException Error in host objects ?
*/
public void putProperty(String propertyName,
ESValue propertyValue,
int hash)
throws EcmaScriptException {
properties.put(propertyName, hash, false, false, propertyValue);
}
/**
* Put the property by index value. By default the index is
* converted to a string, but this can be optimized for arrays.
* <P>This is not the same as the indexed properties of the first
* version of JavaScript and does not allow to access named
* properties other than the property using the integer string
* representation as a name.
*
* @param index The property name as an integer.
* @param propertyValue The value to put
* @exception EcmaScriptException Error in host objects ?
*/
public void putProperty(int index, ESValue propertyValue)
throws EcmaScriptException {
String iString = Integer.toString(index);
putProperty(iString, propertyValue, iString.hashCode());
}
/**
* Put the property as hidden. This is mostly used by initialization
* code, so a hash value is computed localy and the string is interned.
*
* @param propertyName The name of the property
* @param propertyValue Its value
* @exception EcmaScriptException Not used
*/
public void putHiddenProperty(String propertyName,
ESValue propertyValue)
throws EcmaScriptException {
propertyName = propertyName.intern();
int hash = propertyName.hashCode();
properties.put(propertyName, hash, true, false, propertyValue);
}
/**
* Implements the [[delete]] function (8.6.2.5), only
* called by the DELETE operator. Should return true if
* the propery does not exist any more (or did not exist
* at all) after the return.
* <P>This routine must implement the DontDelete attribue too.
*
* @param propertyName The name of the property
* @return true if the property is not present anymore
* @exception EcmaScriptException Not used
*/
public boolean deleteProperty(String propertyName, int hash)
throws EcmaScriptException {
properties.remove(propertyName, hash);
return true; // either it did not exist or was deleted !
}
/**
* Implements [[DefaultValue]] with hint
*
* @param hint A type hint (only string or number)
* @exception EcmaScriptException Propagated or bad hint
* @return the default value of this object
*/
public ESValue getDefaultValue(int hint)
throws EcmaScriptException {
ESValue theResult = null;
ESValue theFunction = null;
if (hint == ESValue.EStypeString) {
theFunction = this.getProperty(TOSTRINGstring,TOSTRINGhash);
if (theFunction instanceof ESObject) {
theResult = theFunction.callFunction(this, new ESValue[0]);
if (theResult.isPrimitive()) {
return theResult;
}
}
theFunction = this.getProperty(VALUEOFstring,VALUEOFhash);
if (theFunction instanceof ESObject) {
theResult = theFunction.callFunction(this, new ESValue[0]);
if (theResult.isPrimitive()) {
return theResult;
}
}
// Throw errror on super to avoid evaluating this with as a string,
// as this is exactly what we cannot do.
throw new EcmaScriptException ("No default value for " + super.toString() + " and hint " + hint);
} else if (hint == ESValue.EStypeNumber) {
theFunction = this.getProperty(VALUEOFstring,VALUEOFhash);
if (theFunction instanceof ESObject) {
theResult = theFunction.callFunction(this, new ESValue[0]);
if (theResult.isPrimitive()) {
return theResult;
}
}
theFunction = this.getProperty(TOSTRINGstring,TOSTRINGhash);
if (theFunction instanceof ESObject) {
theResult = theFunction.callFunction(this, new ESValue[0]);
if (theResult.isPrimitive()) {
return theResult;
}
}
}
throw new EcmaScriptException ("No default value for " + this + " and hint " + hint);
}
/**
* Implements [[DefaultValue]] with no hint
* <P> The default is different for dates
*
* @exception EcmaScriptException Propagated
* @return the default value of this object
*/
public ESValue getDefaultValue()
throws EcmaScriptException {
return this.getDefaultValue(EStypeNumber);
}
/**
* Call a function object - not implemented for default objecr
*
* @param thisObject The current object
* @param arguments The arguments to the function
* @return The calculated value
* @exception EcmaScriptException thrown because the function is not implemented
*/
public ESValue callFunction(ESObject thisObject,
ESValue[] arguments)
throws EcmaScriptException {
throw new EcmaScriptException("No function defined on: " + this);
}
/**
* A construct as thisObject.functionName() was detected,
* The functionName is looked up, then a call is made.
* This avoid creating a dummy function object when one does not
* exists, like for the ESWrapper objects (where functions are
* really java methods).
* <P>Only method which do not use the standard EcmaScript
* function evaluation mechanism need to override this method.
*
* @param evaluator The evaluator
* @param target The original target (for recursive calls)
* @param functionName The name of the function property
* @param arguments The arguments of the function
* @return The result of calling the function
* @exception EcmaScriptException Function not defined
* @exception NoSuchMethodException Method not found
*/
public ESValue doIndirectCall(Evaluator evaluator,
ESObject target,
String functionName,
ESValue[] arguments)
throws EcmaScriptException, NoSuchMethodException {
ESValue theFunction = (ESValue) properties.get(functionName, functionName.hashCode());
if (theFunction == null) {
if (prototype == null) {
throw new EcmaScriptException("The function '"+functionName+
"' is not defined for object '"+target.toString()+"'");
} else {
return prototype.doIndirectCall(evaluator, target, functionName, arguments);
}
} else {
return theFunction.callFunction(target,arguments);
}
}
// A routine which may return a function as the value of a builtin
// property must override this function
public ESValue doIndirectCallInScope(Evaluator evaluator,
ScopeChain previousScope,
ESObject thisObject,
String functionName,
int hash,
ESValue[] arguments)
throws EcmaScriptException {
ESValue theFunction = (ESValue) properties.get(functionName, hash);
if (theFunction == null) {
if (previousScope == null) {
throw new EcmaScriptException("no global function named '" + functionName + "'");
} else {
return previousScope.doIndirectCall(evaluator, thisObject, functionName, hash, arguments);
}
}
return theFunction.callFunction(thisObject,arguments);
}
/**
* Call the constructor - not implemented on default object
*
* @param thisObject The current object
* @param arguments Arguments to new
* @return The created obbjecr
* @exception EcmaScriptException thrown because this function is not implemented
*/
public ESObject doConstruct(ESObject thisObject,
ESValue[] arguments)
throws EcmaScriptException {
throw new EcmaScriptException("No constructor defined on: " + this);
}
/**
* Return a double value for this object if possible
*
* @return The double value
* @exception EcmaScriptException If not a suitable primitive
*/
public double doubleValue() throws EcmaScriptException {
ESValue value = ESUndefined.theUndefined;
double d = Double.NaN;
try {
value = toESPrimitive(EStypeNumber);
d = value.doubleValue();
} catch (EcmaScriptException e) {
throw new ProgrammingError(e.getMessage());
}
return d;
}
/**
* Return the boolean value of this object if possible
*
* @return the boolean value
* @exception EcmaScriptException If not a suitable primitive
*/
public boolean booleanValue() throws EcmaScriptException {
return true;
}
public String toString() {
ESValue value = ESUndefined.theUndefined;
String string = null;
try {
value = toESPrimitive(EStypeString);
} catch (EcmaScriptException e) {
return this.toDetailString();
}
string = value.toString();
return string;
}
/**
* Convert to an object
*
* @param evaluator The evaluator
* @return This
* @exception EcmaScriptException not thrown
*/
public final ESValue toESObject(Evaluator evaluator) throws EcmaScriptException {
return this;
}
/**
* Convert to a primitive
*
* @param preferedType For string or number
* @return The primitive value
* @exception EcmaScriptException If no suitable default value
*/
public final ESValue toESPrimitive(int preferedType) throws EcmaScriptException {
return getDefaultValue(preferedType);
}
/**
* Convert to a primitive
*
* @return The primitive value
* @exception EcmaScriptException If no suitable default value
*/
public final ESValue toESPrimitive() throws EcmaScriptException {
return getDefaultValue();
}
/**
* Return a Java object which is the object to pass to Java routines
* called by FESI. By default wrap the ESObject in a wrapper object,
* used by the jslib. Overriden by subclass if a better type can be found.
*
* @return a wrapper object over this ESObject.
*/
public Object toJavaObject() {
return new JSWrapper(this, evaluator);
}
/**
* Return the name of the type of the object for the typeof operator
*
* @return The name of the type as a String
*/
public String getTypeofString() {
return "object";
}
public String toDetailString() {
return "ES:[" + getESClassName() + "]";
}
/**
* Return true to indicate that this value is composite.
*
* @return true
*/
public boolean isComposite() {return true; }
/**
* Return the list of proprties which are not listed by getAll,
* that is all special properties handled directly by getProperty,
* which are not in the property hash table (they are considered
* hidden)
* Must be overriden by a subclass which overrides getProperty!
*
* return The array of special property names
*/
public String[] getSpecialPropertyNames() {
return new String[0];
}
/**
* Get an enumeration of the description of various aspects
* of the object, including all properties.
*/
public Enumeration getAllDescriptions() {
return new Enumeration() {
String [] specialProperties = getSpecialPropertyNames();
int specialEnumerator = 0;
Enumeration props = properties.keys();
String currentKey = null;
int currentHash = 0;
boolean inside = false;
boolean inSpecial = true;
public boolean hasMoreElements() {
// If we have one already, send it
if (currentKey != null) return true;
// Loop on special properties first
if (specialEnumerator < specialProperties.length) {
currentKey = specialProperties[specialEnumerator];
currentHash = currentKey.hashCode();
specialEnumerator++;
return true;
}
inSpecial = false;
// Otherwise check in current enumeration
while (props.hasMoreElements()) {
currentKey = (String) props.nextElement();
currentHash = currentKey.hashCode();
//if (inside) {
// if (properties.containsKey(currentKey, currentHash)) continue;
//}
return true;
}
// Got to prototype enumeration if needed
if (!inside && prototype != null) {
inside = true;
props = prototype.getProperties();
while (props.hasMoreElements()) {
currentKey = (String) props.nextElement();
currentHash = currentKey.hashCode();
//if (properties.containsKey(currentKey, currentHash)) continue;
return true;
}
}
return false;
}
public Object nextElement() {
if (hasMoreElements()) {
String key = currentKey;
int hash = key.hashCode();
currentKey = null;
ESValue value = null;
try {
value = ESObject.this.getProperty(key, hash);
} catch (EcmaScriptException e) {
throw new ProgrammingError("Unexpected exception " + e);
}
String propertyKind;
if (inSpecial) {
propertyKind = "HIDDEN";
} else if (inside && properties.containsKey(key, hash)) {
propertyKind = "INVISIBLE";
} else {
propertyKind = isHiddenProperty(key, hash) ? "HIDDEN" : "VISIBLE";
}
propertyKind += (inside ? " PROTOTYPE" : " OBJECT");
propertyKind += " PROPERTY";
return new ValueDescription(key,
propertyKind,
value.toString());
} else {
throw new java.util.NoSuchElementException();
}
}
};
}
/**
* Returns a full description of the value, with the specified name.
*
* @param name The name of the value to describe
*
* @return the description of this value
*/
public ValueDescription getDescription(String name) {
return new ValueDescription(name,
"OBJECT",
this.toString());
}
}