/* * 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 Apr 10, 2007. */ package com.scratchdisk.script.rhino; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.util.IdentityHashMap; import java.util.Map; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; import org.mozilla.javascript.WrapFactory; import org.mozilla.javascript.Wrapper; import com.scratchdisk.list.ReadOnlyList; import com.scratchdisk.script.ArgumentReader; import com.scratchdisk.script.Callable; import com.scratchdisk.script.Converter; import com.scratchdisk.script.StringArgumentReader; import com.scratchdisk.util.ClassUtils; import com.scratchdisk.util.WeakIdentityHashMap; /** * @author lehni */ public class RhinoWrapFactory extends WrapFactory implements Converter { private WeakIdentityHashMap<Object, WeakReference<Scriptable>> wrappers = new WeakIdentityHashMap<Object, WeakReference<Scriptable>>(); protected RhinoEngine engine; public RhinoWrapFactory() { this.setJavaPrimitiveWrap(false); } /** * wrapCustom should wrap all objects that it would like to be cached in * the wrappers WeakIdentityHashMap. If it returns null, a temporary * ExtendedJavaObject wrapper is created which is not cached. * This is used for example to allow the definition of JS wrappers for File, * which in itself then explicitly create java.io.File objects for the * same file, which would otherwise then already be in wrappers and returned. */ public Scriptable wrapCustom(Context cx, Scriptable scope, Object javaObj, Class<?> staticType, boolean newObject) { return new ExtendedJavaObject(scope, javaObj, staticType, true); } public Object wrap(Context cx, Scriptable scope, Object obj, Class<?> staticType) { if (obj == null || obj == Undefined.instance || obj instanceof Scriptable) return obj; if (obj instanceof RhinoCallable) { // Handle the ScriptFunction special case, return the unboxed // function value. obj = ((RhinoCallable) obj).getCallable(); } // Allays override staticType and set it to the native type of // the class. Sometimes the interface used to access an object of // a certain class is passed. // But why should it be wrapped that way? if (staticType == null || !staticType.isPrimitive()) staticType = obj.getClass(); Object result = staticType != null && staticType.isArray() ? new ExtendedJavaArray(scope, obj, staticType, true) : super.wrap(cx, scope, obj, staticType); return result; } public Scriptable wrapNewObject(Context cx, Scriptable scope, Object obj) { if (obj instanceof Scriptable) return (Scriptable) obj; // TODO: Pass as boolean variable instead and change Rhino further cx.putThreadLocal("newObject", true); try { return wrapAsJavaObject(cx, scope, obj, null, true); } finally { cx.removeThreadLocal("newObject"); } } public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObj, Class<?> staticType, boolean newObject) { // Keep track of wrappers so that if a given object needs to be // wrapped again, take the wrapper from the pool... WeakReference<Scriptable> ref = wrappers.get(javaObj); Scriptable obj = ref == null ? null : ref.get(); if (obj == null) { boolean cache = true; // Allays override staticType and set it to the native type // of the class. Sometimes the interface used to access an // object of a certain class is passed. But why should it // be wrapped that way? staticType = javaObj.getClass(); if (staticType != null && staticType.isArray()) obj = new ExtendedJavaArray(scope, javaObj, staticType, true); else { if (javaObj instanceof ReadOnlyList) { obj = new ListWrapper(scope, (ReadOnlyList) javaObj, staticType, true); } else if (javaObj instanceof Map) { obj = new MapWrapper(scope, (Map) javaObj); } else { obj = wrapCustom(cx, scope, javaObj, staticType, newObject); if (obj == null) { obj = new ExtendedJavaObject(scope, javaObj, staticType, true); // See the comment in wrapCustom for an explanation of // this: cache = false; } } } if (cache) wrappers.put(javaObj, new WeakReference<Scriptable>(obj)); } return obj; } private IdentityHashMap<Class, IdentityHashMap<Class, Integer>> conversionCache = new IdentityHashMap<Class, IdentityHashMap<Class, Integer>>(); /** * getConversionWeight is defined here to only calculate the weight per * from-to class pair once, after that it is cached in the conversionTable * and retrieved from there. calculateConversionWeight is used instead for * the calculations. */ public int getConversionWeight(Object from, Object unwrapped, Class<?> to, int defaultWeight) { IdentityHashMap<Class, Integer> fromCache = null; if (unwrapped != null) { Class fromClass = unwrapped.getClass(); fromCache = conversionCache.get(fromClass); if (fromCache == null) { fromCache = new IdentityHashMap<Class, Integer>(); conversionCache.put(fromClass, fromCache); } Integer res = fromCache.get(to); if (res != null) return res; } int weight = calculateConversionWeight(from, unwrapped, to, defaultWeight); if (fromCache != null) { fromCache.put(to, weight); } return weight; } /** * getConversionWeight above is defined to call calculateConversionWeight * and cache the results. Do not override getConversionWeight in any * subclasses, override calculateConversionWeight instead. */ public int calculateConversionWeight(Object from, Object unwrapped, Class<?> to, int defaultWeight) { // See if object "from" can be converted to an instance of class "to" // by the use of a map constructor or the setting of all the fields // of a NativeObject on the instance after its creation, // all added features of JS in Scriptographer: boolean isString = false; // Let through string as well, for ArgumentReader if (from instanceof Scriptable || (isString = from instanceof String)) { // The preferred conversion is from a native object / array to // a class that supports an ArgumentReader constructor. // Everything else is less preferred (even conversion using // the same constructor and another Scriptable object, e.g. // a wrapped Java object). boolean isNativeObject = from instanceof NativeObject; if (isNativeObject || from instanceof NativeArray || isString) { if (ArgumentReader.class.isAssignableFrom(to)) return CONVERSION_TRIVIAL + 1; else if (ArgumentReader.canConvert(to)) return CONVERSION_TRIVIAL + 2; } if (isNativeObject) { if (Map.class.isAssignableFrom(to)) { // If there are two version of a method, e.g. one with Map // and the other with EnumMap prefer the more general one: return Map.class.equals(to) ? CONVERSION_TRIVIAL + 1 : CONVERSION_TRIVIAL + 2; } // Try and see if unwrapping NativeObjects through JS unwrap // method brings us to the right type. unwrapped = unwrap(from); // TODO: Should this be run through calculateConversionWeight // again? // TODO: The result of this should not be cached under the // wrapper type as it will permanently link NativeObject to the // result. This should be achieved by never caching results when // from is a NativeObject. if (unwrapped != from && to.isInstance(unwrapped)) return CONVERSION_TRIVIAL; } else if (!isString) { // String and ArgumentReader we tried above already if (getZeroArgumentConstructor(to) != null || ArgumentReader.canConvert(to)) { // Now if there are more options here to convert from, e.g. // Size and Point prefer the one that has the same simple // name, to encourage conversion between ADM and AI Size, // Rectangle, Point objects! return unwrapped.getClass().getSimpleName().equals( to.getSimpleName()) ? CONVERSION_TRIVIAL + 1 : CONVERSION_TRIVIAL + 2; } } } return defaultWeight; } protected ArgumentReader getArgumentReader(Object object) { if (object instanceof NativeArray) return new ArrayArgumentReader(this, (NativeArray) object); else if (object instanceof Scriptable) return new MapArgumentReader(this, (Scriptable) object); else if (object instanceof String) return new StringArgumentReader(this, (String) object); return null; } public void setProperties(Object object, ArgumentReader reader) { // Similar to ExtendedJavaClass#setProperties, but we need to wrap the // result in a Scriptable object so we can rely on Rhino to set all // properties on it. It will automatically find the right setters for us // and use all value conversion mechanisms available. Scriptable scriptable = wrapNewObject( Context.getCurrentContext(), engine.getScope(), object); for (Object id : reader.keys()) scriptable.put((String) id, scriptable, reader.readObject( id.toString())); } public Object coerceType(Class<?> type, Object value, Object unwrapped) { // Coerce native objects to maps when needed if (value instanceof Function) { if (type == Callable.class) return new RhinoCallable(engine, (Function) value); } else if (value instanceof Scriptable || value instanceof String) { // Let through string as well, for ArgumentReader // TODO: Add support for constructor detection that receives the // passed value, or can convert to it. if (Map.class.isAssignableFrom(type)) return toMap((Scriptable) value); // Try and see if unwrapping NativeObjects through JS unwrap // method brings us to the right type. boolean isNativeObject = value instanceof NativeObject; if (isNativeObject) { unwrapped = unwrap(value); if (unwrapped != value && type.isInstance(unwrapped)) return unwrapped; } ArgumentReader reader = null; if (ArgumentReader.canConvert(type) && (reader = getArgumentReader(value)) != null) { return ArgumentReader.convert(reader, unwrapped, type, this); } else if (isNativeObject) { Constructor ctor = getZeroArgumentConstructor(type); if (ctor != null) { try { Object result = ctor.newInstance(); // As a conversion, use setProperties through argument // reader to set all values on the newly // created object. setProperties(result, getArgumentReader(value)); return result; } catch (Exception e) { throw Context.throwAsScriptRuntimeEx(e); } } } } else if (value == Undefined.instance) { // Convert undefined to false if destination is boolean if (type == Boolean.TYPE) return Boolean.FALSE; } else if (value instanceof Boolean) { // Convert false to null / undefined for non primitive destination // classes. if (!((Boolean) value).booleanValue() && !type.isPrimitive()) return Undefined.instance; } return null; } @SuppressWarnings("unchecked") public <T> T convert(Object from, Class<T> to) { return (T) Context.jsToJava(from, to); } public Object unwrap(Object obj) { if (obj instanceof Wrapper) { return ((Wrapper) obj).unwrap(); } else if (obj instanceof NativeObject) { // Allow JS objects to define a unwrap method: NativeObject object = (NativeObject) obj; Object unwrap = ScriptableObject.getProperty(object, "unwrap"); if (unwrap != Scriptable.NOT_FOUND && unwrap instanceof org.mozilla.javascript.Callable) { obj = ((org.mozilla.javascript.Callable) unwrap).call( Context.getCurrentContext(), engine.topLevel, object, ScriptRuntime.emptyArgs); if (obj != object) return unwrap(obj); } } return obj; } /** * Takes a scriptable and either wraps it in a MapAdapter or unwraps a map * within it if it is a MapWrapper. This avoids multiple wrapping of * MapWrappers and MapAdapters * * @param scriptable * @return a map object representing the passed scriptable. */ private Map toMap(Scriptable scriptable) { if (scriptable instanceof MapWrapper) return (Map) ((MapWrapper) scriptable).unwrap(); return new MapAdapter(scriptable); } /** * Determines whether the class has a zero argument constructor or not. * A cache is used to speed up lookup. * * @param cls * @return true if the class has a zero argument constructor, false * otherwise. */ private static Constructor getZeroArgumentConstructor(Class<?> cls) { return ClassUtils.getConstructor(cls, new Class[] { }, zeroArgumentConstructors); } private static IdentityHashMap<Class, Constructor> zeroArgumentConstructors = new IdentityHashMap<Class, Constructor>(); }