/*
* Scriptographer
*
* This file is part of Scriptographer, a Scripting Plugin for Adobe Illustrator
* http://scriptographer.org/
*
* Copyright (c) 2002-2010, Juerg Lehni
* http://scratchdisk.com/
*
* All rights reserved. See LICENSE file for details.
*
* File created on 01.01.2005.
*/
package com.scratchdisk.script.rhino;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.MemberBox;
import org.mozilla.javascript.NativeJavaClass;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import com.scratchdisk.script.ArgumentReader;
/**
* @author lehni
*/
public class ExtendedJavaClass extends NativeJavaClass {
private String className;
private HashMap<String, Object> properties;
private Scriptable instanceProto = null;
// A lookup for the associated ExtendedJavaClass wrappers
private static IdentityHashMap<Class, ExtendedJavaClass> classes =
new IdentityHashMap<Class, ExtendedJavaClass>();
public ExtendedJavaClass(Scriptable scope, Class cls, boolean unsealed) {
super(scope, cls);
// Set the function prototype, as basically Java constructors
// behave like JS constructor functions. Like this, all properties
// from Function.prototype are inherited.
setParentScope(scope);
setPrototype(ScriptableObject.getFunctionPrototype(scope));
// Determine short className:
className = cls.getSimpleName();
properties = unsealed ? new HashMap<String, Object>() : null;
// put it in the class wrapper table
classes.put(cls, this);
}
public Scriptable construct(Context cx, Scriptable scope, Object[] args) {
// If the normal constructor failed, try to see if the last
// argument is a Callable or a NativeObject object.
// If it is a NativeObject, use it as a hashtable containing
// fields to be added to the object. If it is a Callable,
// call it on the object and again use its return value
// as a hashtable if it is a NativeObject.
Class classObject = getClassObject();
int modifiers = classObject.getModifiers();
NativeObject properties = null;
Callable initialize = null;
if (args.length > 0
&& !Modifier.isInterface(modifiers)
&& !Modifier.isAbstract(modifiers)) {
// Look at the last argument to find out if we need to do something
// special. Possibilities: a object literal that defines fields to
// be set, or a function that is executed on the object and of which
// the result can be fields to be set.
Object last = args[args.length - 1];
// Match callables for initialize functions but filter out java constructors
// which might be arguments to methods...
if (last instanceof Callable && !(last instanceof NativeJavaClass))
initialize = (Callable) last;
else if (last instanceof NativeObject) {
// Now see if the constructor takes a Map as the last argument.
// If so, the NativeObject will be converted to it thought
// RhinoWrapFactory. Otherwise, the NativeObject is used
// as the properties to be set on the instance after creation.
MemberBox ctor = findConstructor(cx, args);
if (ctor != null) {
Class[] types = ctor.ctor().getParameterTypes();
Class lastType = types[types.length - 1];
// Only set the property object if the constructor does
// not expect an ArgumentReader or a Map object, both
// of which NativeObject's can be converted to.
if (!ArgumentReader.class.isAssignableFrom(lastType)
&& !Map.class.isAssignableFrom(lastType)) {
properties = (NativeObject) last;
if (ScriptableObject.hasProperty(properties, "unwrap"))
properties = null;
}
} else {
// There is no constructor that has to be checked, so it
// can only be a properties list.
properties = (NativeObject) last;
}
if (properties != null) {
// Support initialize in the passed object literal too.
Object obj = ScriptableObject.getProperty(properties, "initialize");
if (obj instanceof Callable)
initialize = (Callable) obj;
}
}
// Remove the last argument from the list, so the right constructor
// will be found:
if (initialize != null || properties != null) {
Object[] newArgs = new Object[args.length - 1];
for (int i = 0; i < newArgs.length; i++)
newArgs[i] = args[i];
args = newArgs;
}
}
Scriptable obj = super.construct(cx, scope, args);
// If properties are to be added, do it now. Add the ones from the
// object literal first, then call initialize and after add the
// properties returned by initialize.
if (properties != null)
setProperties(obj, properties);
// If an initialize function was passed as the last argument, execute
// it now. The fields of the result of the function are then injected
// into the object after, if it is a NativeObject.
if (initialize != null) {
Object res = initialize.call(cx, scope, obj, args);
if (res instanceof NativeObject)
setProperties(obj, (NativeObject) res);
}
return obj;
}
private void setProperties(Scriptable obj, NativeObject properties) {
for (Object id : properties.getIds()) {
if (id instanceof String && !id.equals("initialize"))
obj.put((String) id, obj, properties.get((String) id, properties));
}
}
public Class<?> getClassObject() {
// Why calling super.unwrap() when all it does is returning the internal
// javaObject? That's how it's done in NativeJavaClass...
return (Class<?>) javaObject;
}
public Object get(String name, Scriptable start) {
// We are completely redefining get here without relying on
// NativeJavaClass' implementation, in order to add more JS
// like behavior.
// When used as a constructor, ScriptRuntime.newObject() asks
// for our prototype to create an object of the correct type.
// We don't really care what the object is, since we're returning
// one constructed out of whole cloth, so we return null.
boolean isProto = name.equals("prototype");
if (!isProto) {
if (members.has(name, true)) {
return members.get(this, name, javaObject, true);
}
// Changing access sequence of members / staticFieldAndMethods
// to be more logical / java-like. TODO: Is this a Rhino bug?
if (staticFieldAndMethods != null) {
Object result = staticFieldAndMethods.get(name);
if (result != null)
return result;
}
}
if (properties != null && properties.containsKey(name)) {
// see whether this object defines the property.
return properties.get(name);
} else if (isProto) {
// getPrototype creates prototype Objects on the fly:
return getInstancePrototype();
} else {
Scriptable proto = getPrototype();
if (proto != null) {
Object result = proto.get(name, start);
if (result != Scriptable.NOT_FOUND)
return result;
}
}
// Experimental: look for nested classes by appending $name to
// current class' name.
Class<?> nestedClass = findNestedClass(getClassObject(), name);
if (nestedClass != null) {
ExtendedJavaClass nestedValue = new ExtendedJavaClass(
ScriptableObject.getTopLevelScope(this),
nestedClass, properties != null);
nestedValue.setParentScope(this);
return nestedValue;
}
return Scriptable.NOT_FOUND;
}
public void put(String name, Scriptable start, Object value) {
if (members.has(name, true)) {
members.put(this, name, javaObject, value, true);
} else if (name.equals("prototype")) {
if (value instanceof Scriptable)
this.setPrototype((Scriptable) value);
} else if (properties != null) {
properties.put(name, value);
}
}
public boolean has(String name, Scriptable start) {
boolean has = members.has(name, true);
if (!has && properties != null)
has = properties.get(name) != null;
return has;
}
public void delete(String name) {
if (properties != null)
properties.remove(name);
}
public Scriptable getInstancePrototype() {
if (instanceProto == null) {
instanceProto = new NativeObject();
// Set the prototype chain correctly for this prototype object,
// so properties in the prototype of parent classes are found too:
Class sup = getClassObject().getSuperclass();
Scriptable parent;
if (sup != null) {
ExtendedJavaClass classWrapper = getClassWrapper(
ScriptableObject.getTopLevelScope(this), sup);
parent = classWrapper.getInstancePrototype();
} else {
// At the end of the chain, there is always the Object prototype.
parent = ScriptableObject.getObjectPrototype(this);
}
instanceProto.setPrototype(parent);
}
return instanceProto;
}
public String getClassName() {
return className;
}
public String toString() {
return "[" + className + "]";
}
protected static ExtendedJavaClass getClassWrapper(Scriptable scope, Class javaClass) {
ExtendedJavaClass cls = classes.get(javaClass);
if (cls == null) {
// Search for the ExtendedJavaClass by splitting the full name into bits
// separated by '.', and walk up the Packages chain:
String[] path = javaClass.getName().split("\\.");
Scriptable global = ScriptableObject.getTopLevelScope(scope);
// Use ScriptableObject.getProperty so it also looks in the prototypes
// of shared scopes.
Object packages = ScriptableObject.getProperty(global, "Packages");
if (packages != Scriptable.NOT_FOUND) {
Scriptable current = (Scriptable) packages;
for (int i = 0; i < path.length; i++)
current = (Scriptable) current.get(path[i], current);
// Now obj needs to be an instance of ExtendedJavaClass.
// Note that we do not need to put it into classes, as the constructor
// does this for us.
cls = (ExtendedJavaClass) current;
}
}
return cls;
}
}