534 lines
21 KiB
Java
534 lines
21 KiB
Java
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
|
*
|
|
* The contents of this file are subject to the Netscape Public License
|
|
* Version 1.0 (the "NPL"); you may not use this file except in
|
|
* compliance with the NPL. You may obtain a copy of the NPL at
|
|
* http://www.mozilla.org/NPL/
|
|
*
|
|
* Software distributed under the NPL is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
|
|
* for the specific language governing rights and limitations under the
|
|
* NPL.
|
|
*
|
|
* The Initial Developer of this code under the NPL is Netscape
|
|
* Communications Corporation. Portions created by Netscape are
|
|
* Copyright (C) 1997-1999 Netscape Communications Corporation. All Rights
|
|
* Reserved.
|
|
*/
|
|
|
|
// API class
|
|
|
|
package org.mozilla.javascript;
|
|
|
|
import java.util.Hashtable;
|
|
import java.util.Vector;
|
|
import java.lang.reflect.*;
|
|
|
|
public class FunctionObject extends NativeFunction {
|
|
|
|
/**
|
|
* Create a JavaScript function object from a Java method.
|
|
*
|
|
* <p>The <code>member</code> argument must be either a java.lang.reflect.Method
|
|
* or a java.lang.reflect.Constructor and must match one of two forms.<p>
|
|
*
|
|
* The first form is a member with zero or more parameters
|
|
* of the following types: Object, String, boolean, Scriptable,
|
|
* byte, short, int, float, or double. The Long type is not supported
|
|
* because the double representation of a long (which is the
|
|
* EMCA-mandated storage type for Numbers) may lose precision.
|
|
* If the member is a Method, the return value must be void or one
|
|
* of the types allowed for parameters.<p>
|
|
*
|
|
* The runtime will perform appropriate conversions based
|
|
* upon the type of the parameter. A parameter type of
|
|
* Object specifies that no conversions are to be done. A parameter
|
|
* of type String will use Context.toString to convert arguments.
|
|
* Similarly, parameters of type double, boolean, and Scriptable
|
|
* will cause Context.toNumber, Context.toBoolean, and
|
|
* Context.toObject, respectively, to be called.<p>
|
|
*
|
|
* If the method is not static, the Java 'this' value will
|
|
* correspond to the JavaScript 'this' value. Any attempt
|
|
* to call the function with a 'this' value that is not
|
|
* of the right Java type will result in an error.<p>
|
|
*
|
|
* The second form is the variable arguments (or "varargs")
|
|
* form. If the FunctionObject will be used as a constructor,
|
|
* the member must have the following parameters
|
|
* <pre>
|
|
* (Context cx, Object[] args, Function ctorObj,
|
|
* boolean inNewExpr)</pre>
|
|
* and if it is a Method, be static and return an Object result.<p>
|
|
*
|
|
* Otherwise, if the FunctionObject will <i>not</i> be used to define a
|
|
* constructor, the member must be a static Method with parameters
|
|
* (Context cx, Scriptable thisObj, Object[] args,
|
|
* Function funObj) </pre>
|
|
* <pre>
|
|
* and an Object result.<p>
|
|
*
|
|
* When the function varargs form is called as part of a function call,
|
|
* the <code>args</code> parameter contains the
|
|
* arguments, with <code>thisObj</code>
|
|
* set to the JavaScript 'this' value. <code>funObj</code>
|
|
* is the function object for the invoked function.<p>
|
|
*
|
|
* When the constructor varargs form is called or invoked while evaluating
|
|
* a <code>new</code> expression, <code>args</code> contains the
|
|
* arguments, <code>ctorObj</code> refers to this FunctionObject, and
|
|
* <code>inNewExpr</code> is true if and only if a <code>new</code>
|
|
* expression caused the call. This supports defining a function that
|
|
* has different behavior when called as a constructor than when
|
|
* invoked as a normal function call. (For example, the Boolean
|
|
* constructor, when called as a function,
|
|
* will convert to boolean rather than creating a new object.)<p>
|
|
*
|
|
* @param name the name of the function
|
|
* @param methodOrConstructor a java.lang.reflect.Method or a java.lang.reflect.Constructor
|
|
* that defines the object
|
|
* @param scope enclosing scope of function
|
|
* @see org.mozilla.javascript.Scriptable
|
|
*/
|
|
public FunctionObject(String name, Member methodOrConstructor,
|
|
Scriptable scope)
|
|
{
|
|
String methodName;
|
|
if (methodOrConstructor instanceof Constructor) {
|
|
ctor = (Constructor) methodOrConstructor;
|
|
isStatic = true; // well, doesn't take a 'this'
|
|
types = ctor.getParameterTypes();
|
|
methodName = ctor.getName();
|
|
} else {
|
|
method = (Method) methodOrConstructor;
|
|
isStatic = Modifier.isStatic(method.getModifiers());
|
|
types = method.getParameterTypes();
|
|
methodName = method.getName();
|
|
}
|
|
String myNames[] = { name };
|
|
super.names = myNames;
|
|
int length;
|
|
if (types.length == 4 && (types[1].isArray() || types[2].isArray())) {
|
|
// Either variable args or an error.
|
|
if (types[1].isArray()) {
|
|
if (!isStatic ||
|
|
types[0] != Context.class ||
|
|
types[1].getComponentType() != ScriptRuntime.ObjectClass ||
|
|
types[2] != ScriptRuntime.FunctionClass ||
|
|
types[3] != Boolean.TYPE)
|
|
{
|
|
String[] args = { methodName };
|
|
String message = Context.getMessage("msg.varargs.ctor",
|
|
args);
|
|
throw Context.reportRuntimeError(message);
|
|
}
|
|
parmsLength = VARARGS_CTOR;
|
|
} else {
|
|
if (!isStatic ||
|
|
types[0] != Context.class ||
|
|
types[1] != ScriptRuntime.ScriptableClass ||
|
|
types[2].getComponentType() != ScriptRuntime.ObjectClass ||
|
|
types[3] != ScriptRuntime.FunctionClass)
|
|
{
|
|
String[] args = { methodName };
|
|
String message = Context.getMessage("msg.varargs.fun",
|
|
args);
|
|
throw Context.reportRuntimeError(message);
|
|
}
|
|
parmsLength = VARARGS_METHOD;
|
|
}
|
|
// XXX check return type
|
|
length = 1;
|
|
} else {
|
|
parmsLength = (short) types.length;
|
|
boolean hasConversions = false;
|
|
for (int i=0; i < parmsLength; i++) {
|
|
Class type = types[i];
|
|
if (type == ScriptRuntime.ObjectClass) {
|
|
// may not need conversions
|
|
} else if (type == ScriptRuntime.StringClass ||
|
|
type == ScriptRuntime.BooleanClass ||
|
|
ScriptRuntime.NumberClass.isAssignableFrom(type) ||
|
|
Scriptable.class.isAssignableFrom(type))
|
|
{
|
|
hasConversions = true;
|
|
} else if (type == Boolean.TYPE) {
|
|
hasConversions = true;
|
|
types[i] = ScriptRuntime.BooleanClass;
|
|
} else if (type == Byte.TYPE) {
|
|
hasConversions = true;
|
|
types[i] = ScriptRuntime.ByteClass;
|
|
} else if (type == Short.TYPE) {
|
|
hasConversions = true;
|
|
types[i] = ScriptRuntime.ShortClass;
|
|
} else if (type == Integer.TYPE) {
|
|
hasConversions = true;
|
|
types[i] = ScriptRuntime.IntegerClass;
|
|
} else if (type == Float.TYPE) {
|
|
hasConversions = true;
|
|
types[i] = ScriptRuntime.FloatClass;
|
|
} else if (type == Double.TYPE) {
|
|
hasConversions = true;
|
|
types[i] = ScriptRuntime.DoubleClass;
|
|
}
|
|
// Note that long is not supported; see comments above
|
|
else {
|
|
Object[] errArgs = { methodName };
|
|
throw Context.reportRuntimeError(
|
|
Context.getMessage("msg.bad.parms", errArgs));
|
|
}
|
|
}
|
|
if (!hasConversions)
|
|
types = null;
|
|
length = parmsLength;
|
|
}
|
|
|
|
// Initialize length property
|
|
lengthPropertyValue = (short) length;
|
|
|
|
hasVoidReturn = method != null && method.getReturnType() == Void.TYPE;
|
|
this.argCount = (short) length;
|
|
|
|
setParentScope(scope);
|
|
setPrototype(getFunctionPrototype(scope));
|
|
}
|
|
|
|
/**
|
|
* Override ScriptableObject's has, get, and set in order to define
|
|
* the "length" property of the function. <p>
|
|
*
|
|
* We could also have defined the property using ScriptableObject's
|
|
* defineProperty method, but that would have consumed a slot in every
|
|
* FunctionObject. Most FunctionObjects typically don't have any
|
|
* properties anyway, so having the "length" property would cause us
|
|
* to allocate an array of slots. <p>
|
|
*
|
|
* In particular, this method will return true for
|
|
* <code>name.equals("length")</code>
|
|
* and will delegate to the superclass for all other
|
|
* values of <code>name</code>.
|
|
*/
|
|
public boolean has(String name, Scriptable start) {
|
|
return name.equals("length") || super.has(name, start);
|
|
}
|
|
|
|
/**
|
|
* Override ScriptableObject's has, get, and set in order to define
|
|
* the "length" property of the function. <p>
|
|
*
|
|
* In particular, this method will return the value defined by
|
|
* the method used to construct the object (number of parameters
|
|
* of the method, or 1 if the method is a "varargs" form), unless
|
|
* setLength has been called with a new value.
|
|
*
|
|
* @see org.mozilla.javascript.FunctionObject#setLength
|
|
*/
|
|
public Object get(String name, Scriptable start) {
|
|
if (name.equals("length"))
|
|
return new Integer(lengthPropertyValue);
|
|
return super.get(name, start);
|
|
}
|
|
|
|
/**
|
|
* Override ScriptableObject's has, get, and set in order to define
|
|
* the "length" property of the function. <p>
|
|
*
|
|
* In particular, this method will ignore all attempts to set the
|
|
* "length" property and forward all other requests to ScriptableObject.
|
|
*
|
|
* @see org.mozilla.javascript.FunctionObject#setLength
|
|
*/
|
|
public void put(String name, Scriptable start, Object value) {
|
|
if (!name.equals("length"))
|
|
super.put(name, start, value);
|
|
}
|
|
|
|
/**
|
|
* Set the value of the "length" property.
|
|
*
|
|
* <p>Changing the value of the "length" property of a FunctionObject only
|
|
* affects the value retrieved from get() and does not affect the way
|
|
* the method itself is called. <p>
|
|
*
|
|
* The "length" property will be defined by default as the number
|
|
* of parameters of the method used to construct the FunctionObject,
|
|
* unless the method is a "varargs" form, in which case the "length"
|
|
* property will be defined to 1.
|
|
*
|
|
* @param length the new length
|
|
*/
|
|
public void setLength(short length) {
|
|
lengthPropertyValue = length;
|
|
}
|
|
|
|
// TODO: Make not public
|
|
/**
|
|
* Finds methods of a given name in a given class.
|
|
*
|
|
* <p>Searches <code>clazz</code> for methods with name
|
|
* <code>name</code>. Maintains a cache so that multiple
|
|
* lookups on the same class are cheap.
|
|
*
|
|
* @param clazz the class to search
|
|
* @param name the name of the methods to find
|
|
* @return an array of the found methods, or null if no methods
|
|
* by that name were found.
|
|
* @see java.lang.Class#getMethods
|
|
*/
|
|
public static Method[] findMethods(Class clazz, String name) {
|
|
Vector v = new Vector(5);
|
|
Method[] methods = clazz.getMethods();
|
|
for (int i=0; i < methods.length; i++) {
|
|
if (methods[i].getDeclaringClass() == clazz &&
|
|
methods[i].getName().equals(name)){
|
|
v.addElement(methods[i]);
|
|
}
|
|
}
|
|
if (v.size() == 0) {
|
|
return null;
|
|
}
|
|
Method[] result = new Method[v.size()];
|
|
v.copyInto(result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Define this function as a JavaScript constructor.
|
|
* <p>
|
|
* Sets up the "prototype" and "constructor" properties. Also
|
|
* calls setParent and setPrototype with appropriate values.
|
|
* Then adds the function object as a property of the given scope, using
|
|
* <code>prototype.getClassName()</code>
|
|
* as the name of the property.
|
|
*
|
|
* @param scope the scope in which to define the constructor (typically
|
|
* the global object)
|
|
* @param prototype the prototype object
|
|
* @see org.mozilla.javascript.Scriptable#setParentScope
|
|
* @see org.mozilla.javascript.Scriptable#setPrototype
|
|
* @see org.mozilla.javascript.Scriptable#getClassName
|
|
*/
|
|
public void addAsConstructor(Scriptable scope, Scriptable prototype) {
|
|
setParentScope(scope);
|
|
setPrototype(getFunctionPrototype(scope));
|
|
prototype.setParentScope(this);
|
|
final int attr = ScriptableObject.DONTENUM |
|
|
ScriptableObject.PERMANENT |
|
|
ScriptableObject.READONLY;
|
|
defineProperty("prototype", prototype, attr);
|
|
String name = prototype.getClassName();
|
|
if (!name.equals("With")) {
|
|
// A "With" object would delegate these calls to the prototype:
|
|
// not the right thing to do here!
|
|
if (prototype instanceof ScriptableObject) {
|
|
((ScriptableObject) prototype).defineProperty("constructor",
|
|
this, attr);
|
|
} else {
|
|
prototype.put("constructor", prototype, this);
|
|
}
|
|
}
|
|
if (scope instanceof ScriptableObject) {
|
|
((ScriptableObject) scope).defineProperty(name, this,
|
|
ScriptableObject.DONTENUM);
|
|
} else {
|
|
scope.put(name, scope, this);
|
|
}
|
|
setParentScope(scope);
|
|
}
|
|
|
|
static public Object convertArg(Scriptable scope,
|
|
Object arg, Class desired)
|
|
{
|
|
if (desired == ScriptRuntime.BooleanClass
|
|
|| desired == Boolean.TYPE)
|
|
return ScriptRuntime.toBoolean(arg) ? Boolean.TRUE
|
|
: Boolean.FALSE;
|
|
else if (desired == ScriptRuntime.StringClass)
|
|
return ScriptRuntime.toString(arg);
|
|
else if (desired == ScriptRuntime.IntegerClass
|
|
|| desired == Integer.TYPE)
|
|
return new Integer(ScriptRuntime.toInt32(arg));
|
|
else if (desired == ScriptRuntime.DoubleClass
|
|
|| desired == Double.TYPE)
|
|
return new Double(ScriptRuntime.toNumber(arg));
|
|
else if (desired == ScriptRuntime.ScriptableClass)
|
|
return ScriptRuntime.toObject(scope, arg);
|
|
// Note that the long type is not supported; see the javadoc for
|
|
// the constructor for this class
|
|
else {
|
|
Object[] errArgs = { desired.getName() };
|
|
throw Context.reportRuntimeError(
|
|
Context.getMessage("msg.cant.convert", errArgs));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs conversions on argument types if needed and
|
|
* invokes the underlying Java method or constructor.
|
|
* <p>
|
|
* Implements Function.call.
|
|
*
|
|
* @see org.mozilla.javascript.Function#call
|
|
* @exception JavaScriptException if the underlying Java method or constructor
|
|
* threw an exception
|
|
*/
|
|
public Object call(Context cx, Scriptable scope, Scriptable thisObj,
|
|
Object[] args)
|
|
throws JavaScriptException
|
|
{
|
|
if (parmsLength < 0)
|
|
return callVarargs(cx, thisObj, args, false);
|
|
|
|
if (!isStatic) {
|
|
// OPT: cache "clazz"?
|
|
Class clazz = method != null ? method.getDeclaringClass()
|
|
: ctor.getDeclaringClass();
|
|
Scriptable p = thisObj;
|
|
while (p != null && !clazz.isInstance(p)) {
|
|
// Walk up the prototype chain to find an object to call the
|
|
// method on
|
|
p = p.getPrototype();
|
|
}
|
|
if (p == null) {
|
|
// Couldn't find an object to call this on.
|
|
Object[] errArgs = { names[0] };
|
|
throw Context.reportRuntimeError(
|
|
Context.getMessage("msg.incompat.call", errArgs));
|
|
}
|
|
thisObj = p;
|
|
}
|
|
Object[] invokeArgs;
|
|
int i;
|
|
if (parmsLength == args.length) {
|
|
invokeArgs = args;
|
|
// avoid copy loop if no conversions needed
|
|
i = (types == null) ? parmsLength : 0;
|
|
} else {
|
|
invokeArgs = new Object[parmsLength];
|
|
i = 0;
|
|
}
|
|
for (; i < parmsLength; i++) {
|
|
Object arg = (i < args.length)
|
|
? args[i]
|
|
: Undefined.instance;
|
|
if (types != null) {
|
|
arg = convertArg(this, arg, types[i]);
|
|
}
|
|
invokeArgs[i] = arg;
|
|
}
|
|
try {
|
|
Object result = (method != null)
|
|
? method.invoke(thisObj, invokeArgs)
|
|
: ctor.newInstance(invokeArgs);
|
|
return hasVoidReturn ? Undefined.instance : result;
|
|
}
|
|
catch (InvocationTargetException e) {
|
|
throw JavaScriptException.wrapException(scope, e);
|
|
}
|
|
catch (IllegalAccessException e) {
|
|
throw WrappedException.wrapException(e);
|
|
}
|
|
catch (InstantiationException e) {
|
|
throw WrappedException.wrapException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs conversions on argument types if needed and
|
|
* invokes the underlying Java method or constructor
|
|
* to create a new Scriptable object.
|
|
* <p>
|
|
* Implements Function.construct.
|
|
*
|
|
* @param cx the current Context for this thread
|
|
* @param scope the scope to execute the function relative to. This
|
|
* set to the value returned by getParentScope() except
|
|
* when the function is called from a closure.
|
|
* @param args arguments to the constructor
|
|
* @see org.mozilla.javascript.Function#construct
|
|
* @exception JavaScriptException if the underlying Java method or constructor
|
|
* threw an exception
|
|
*/
|
|
public Scriptable construct(Context cx, Scriptable scope, Object[] args)
|
|
throws JavaScriptException
|
|
{
|
|
if (method == null || parmsLength == VARARGS_CTOR) {
|
|
Scriptable result;
|
|
if (method != null) {
|
|
// Ugly: allow variable-arg constructors that need access to the
|
|
// scope to get it from the Context. Cleanest solution would be
|
|
// to modify the varargs form, but that would require users with
|
|
// the old form to change their code.
|
|
cx.ctorScope = scope;
|
|
result = (Scriptable) callVarargs(cx, null, args, true);
|
|
cx.ctorScope = null;
|
|
} else {
|
|
result = (Scriptable) call(cx, scope, null, args);
|
|
}
|
|
|
|
if (result.getPrototype() == null)
|
|
result.setPrototype(getClassPrototype());
|
|
if (result.getParentScope() == null) {
|
|
Scriptable parent = getParentScope();
|
|
if (result != parent)
|
|
result.setParentScope(parent);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return super.construct(cx, scope, args);
|
|
}
|
|
|
|
private Object callVarargs(Context cx, Scriptable thisObj, Object[] args,
|
|
boolean inNewExpr)
|
|
throws JavaScriptException
|
|
{
|
|
try {
|
|
if (parmsLength == VARARGS_METHOD) {
|
|
Object[] invokeArgs = { cx, thisObj, args, this };
|
|
Object result = method.invoke(null, invokeArgs);
|
|
return hasVoidReturn ? Undefined.instance : result;
|
|
} else {
|
|
Boolean b = inNewExpr ? Boolean.TRUE : Boolean.FALSE;
|
|
Object[] invokeArgs = { cx, args, this, b };
|
|
return (method == null)
|
|
? ctor.newInstance(invokeArgs)
|
|
: method.invoke(null, invokeArgs);
|
|
}
|
|
}
|
|
catch (InvocationTargetException e) {
|
|
Throwable target = e.getTargetException();
|
|
if (target instanceof EvaluatorException)
|
|
throw (EvaluatorException) target;
|
|
Scriptable scope = thisObj == null ? this : thisObj;
|
|
throw JavaScriptException.wrapException(scope, target);
|
|
}
|
|
catch (IllegalAccessException e) {
|
|
throw WrappedException.wrapException(e);
|
|
}
|
|
catch (InstantiationException e) {
|
|
throw WrappedException.wrapException(e);
|
|
}
|
|
}
|
|
|
|
boolean isVarArgsMethod() {
|
|
return parmsLength == VARARGS_METHOD;
|
|
}
|
|
|
|
boolean isVarArgsConstructor() {
|
|
return parmsLength == VARARGS_CTOR;
|
|
}
|
|
|
|
private static final short VARARGS_METHOD = -1;
|
|
private static final short VARARGS_CTOR = -2;
|
|
|
|
Method method;
|
|
Constructor ctor;
|
|
private Class[] types;
|
|
private short parmsLength;
|
|
private short lengthPropertyValue;
|
|
private boolean hasVoidReturn;
|
|
private boolean isStatic;
|
|
}
|