/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Rhino code, released * May 6, 1999. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1997-2000 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Norris Boyd * Igor Bukanov * Frank Mitchell * Mike Shaver * Kemal Bayram * * Alternatively, the contents of this file may be used under the terms of * the GNU General Public License Version 2 or later (the "GPL"), in which * case the provisions of the GPL are applicable instead of those above. If * you wish to allow use of your version of this file only under the terms of * the GPL and not to allow others to use your version of this file under the * MPL, indicate your decision by deleting the provisions above and replacing * them with the notice and other provisions required by the GPL. If you do * not delete the provisions above, a recipient may use your version of this * file under either the MPL or the GPL. * * ***** END LICENSE BLOCK ***** */ package org.mozilla.javascript; import java.io.*; import java.lang.reflect.*; import java.util.Hashtable; import java.util.Date; /** * This class reflects non-Array Java objects into the JavaScript environment. It * reflect fields directly, and uses NativeJavaMethod objects to reflect (possibly * overloaded) methods.
* * @author Mike Shaver * @see NativeJavaArray * @see NativeJavaPackage * @see NativeJavaClass */ public class NativeJavaObject implements Scriptable, Wrapper, Serializable { static final long serialVersionUID = -6948590651130498591L; public NativeJavaObject() { } public NativeJavaObject(Scriptable scope, Object javaObject, Class staticType) { this(scope, javaObject, staticType, false); } public NativeJavaObject(Scriptable scope, Object javaObject, Class staticType, boolean isAdapter) { this.parent = ScriptableObject.getVeryTopLevelScope(scope); // APPJET this.javaObject = javaObject; this.staticType = staticType; this.isAdapter = isAdapter; initMembers(); } protected void initMembers() { Class dynamicType; if (javaObject != null) { dynamicType = javaObject.getClass(); } else { dynamicType = staticType; } members = JavaMembers.lookupClass(parent, dynamicType, staticType, isAdapter); fieldAndMethods = members.getFieldAndMethodsObjects(this, javaObject, false); } public boolean has(String name, Scriptable start) { return members.has(name, false); } public boolean has(int index, Scriptable start) { return false; } public Object get(String name, Scriptable start) { if (fieldAndMethods != null) { Object result = fieldAndMethods.get(name); if (result != null) { return result; } } // TODO: passing 'this' as the scope is bogus since it has // no parent scope return members.get(this, name, javaObject, false); } public Object get(int index, Scriptable start) { throw members.reportMemberNotFound(Integer.toString(index)); } public void put(String name, Scriptable start, Object value) { // We could be asked to modify the value of a property in the // prototype. Since we can't add a property to a Java object, // we modify it in the prototype rather than copy it down. if (prototype == null || members.has(name, false)) members.put(this, name, javaObject, value, false); else prototype.put(name, prototype, value); } public void put(int index, Scriptable start, Object value) { throw members.reportMemberNotFound(Integer.toString(index)); } public boolean hasInstance(Scriptable value) { // This is an instance of a Java class, so always return false return false; } public void delete(String name) { } public void delete(int index) { } public Scriptable getPrototype() { if (prototype == null && javaObject instanceof String) { return ScriptableObject.getClassPrototype(parent, "String"); } return prototype; } /** * Sets the prototype of the object. */ public void setPrototype(Scriptable m) { prototype = m; } /** * Returns the parent (enclosing) scope of the object. */ public Scriptable getParentScope() { return parent; } /** * Sets the parent (enclosing) scope of the object. */ public void setParentScope(Scriptable m) { parent = m; } public Object[] getIds() { return members.getIds(false); } /** @deprecated Use {@link Context#getWrapFactory()} together with calling {@link WrapFactory#wrap(Context, Scriptable, Object, Class)} */ public static Object wrap(Scriptable scope, Object obj, Class staticType) { Context cx = Context.getContext(); return cx.getWrapFactory().wrap(cx, scope, obj, staticType); } public Object unwrap() { return javaObject; } public String getClassName() { return "JavaObject"; } public Object getDefaultValue(Class hint) { Object value; if (hint == null) { if (javaObject instanceof Boolean) { hint = ScriptRuntime.BooleanClass; } } if (hint == null || hint == ScriptRuntime.StringClass) { value = javaObject.toString(); } else { String converterName; if (hint == ScriptRuntime.BooleanClass) { converterName = "booleanValue"; } else if (hint == ScriptRuntime.NumberClass) { converterName = "doubleValue"; } else { throw Context.reportRuntimeError0("msg.default.value"); } Object converterObject = get(converterName, this); if (converterObject instanceof Function) { Function f = (Function)converterObject; value = f.call(Context.getContext(), f.getParentScope(), this, ScriptRuntime.emptyArgs); } else { if (hint == ScriptRuntime.NumberClass && javaObject instanceof Boolean) { boolean b = ((Boolean)javaObject).booleanValue(); value = ScriptRuntime.wrapNumber(b ? 1.0 : 0.0); } else { value = javaObject.toString(); } } } return value; } /** * Determine whether we can/should convert between the given type and the * desired one. This should be superceded by a conversion-cost calculation * function, but for now I'll hide behind precedent. */ public static boolean canConvert(Object fromObj, Class to) { int weight = getConversionWeight(fromObj, to); return (weight < CONVERSION_NONE); } private static final int JSTYPE_UNDEFINED = 0; // undefined type private static final int JSTYPE_NULL = 1; // null private static final int JSTYPE_BOOLEAN = 2; // boolean private static final int JSTYPE_NUMBER = 3; // number private static final int JSTYPE_STRING = 4; // string private static final int JSTYPE_JAVA_CLASS = 5; // JavaClass private static final int JSTYPE_JAVA_OBJECT = 6; // JavaObject private static final int JSTYPE_JAVA_ARRAY = 7; // JavaArray private static final int JSTYPE_OBJECT = 8; // Scriptable static final byte CONVERSION_TRIVIAL = 1; static final byte CONVERSION_NONTRIVIAL = 0; static final byte CONVERSION_NONE = 99; /** * Derive a ranking based on how "natural" the conversion is. * The special value CONVERSION_NONE means no conversion is possible, * and CONVERSION_NONTRIVIAL signals that more type conformance testing * is required. * Based on * * "preferred method conversions" from Live Connect 3 */ static int getConversionWeight(Object fromObj, Class to) { int fromCode = getJSTypeCode(fromObj); switch (fromCode) { case JSTYPE_UNDEFINED: if (to == ScriptRuntime.StringClass || to == ScriptRuntime.ObjectClass) { return 1; } break; case JSTYPE_NULL: if (!to.isPrimitive()) { return 1; } break; case JSTYPE_BOOLEAN: // "boolean" is #1 if (to == Boolean.TYPE) { return 1; } else if (to == ScriptRuntime.BooleanClass) { return 2; } else if (to == ScriptRuntime.ObjectClass) { return 3; } else if (to == ScriptRuntime.StringClass) { return 4; } break; case JSTYPE_NUMBER: if (to.isPrimitive()) { if (to == Double.TYPE) { return 1; } else if (to != Boolean.TYPE) { return 1 + getSizeRank(to); } } else { if (to == ScriptRuntime.StringClass) { // native numbers are #1-8 return 9; } else if (to == ScriptRuntime.ObjectClass) { return 10; } else if (ScriptRuntime.NumberClass.isAssignableFrom(to)) { // "double" is #1 return 2; } } break; case JSTYPE_STRING: if (to == ScriptRuntime.StringClass) { return 1; } else if (to.isInstance(fromObj)) { return 2; } else if (to.isPrimitive()) { if (to == Character.TYPE) { return 3; } else if (to != Boolean.TYPE) { return 4; } } break; case JSTYPE_JAVA_CLASS: if (to == ScriptRuntime.ClassClass) { return 1; } else if (to == ScriptRuntime.ObjectClass) { return 3; } else if (to == ScriptRuntime.StringClass) { return 4; } break; case JSTYPE_JAVA_OBJECT: case JSTYPE_JAVA_ARRAY: Object javaObj = fromObj; if (javaObj instanceof Wrapper) { javaObj = ((Wrapper)javaObj).unwrap(); } if (to.isInstance(javaObj)) { return CONVERSION_NONTRIVIAL; } if (to == ScriptRuntime.StringClass) { return 2; } else if (to.isPrimitive() && to != Boolean.TYPE) { return (fromCode == JSTYPE_JAVA_ARRAY) ? CONVERSION_NONE : 2 + getSizeRank(to); } break; case JSTYPE_OBJECT: // Other objects takes #1-#3 spots if (to == fromObj.getClass()) { // No conversion required return 1; } if (to.isArray()) { if (fromObj instanceof NativeArray) { // This is a native array conversion to a java array // Array conversions are all equal, and preferable to object // and string conversion, per LC3. return 1; } } else if (to == ScriptRuntime.ObjectClass) { return 2; } else if (to == ScriptRuntime.StringClass) { return 3; } else if (to == ScriptRuntime.DateClass) { if (fromObj instanceof NativeDate) { // This is a native date to java date conversion return 1; } } else if (to.isInterface()) { if (fromObj instanceof Function) { // See comments in coerceType if (to.getMethods().length == 1) { return 1; } } return 11; } else if (to.isPrimitive() && to != Boolean.TYPE) { return 3 + getSizeRank(to); } break; } return CONVERSION_NONE; } static int getSizeRank(Class aType) { if (aType == Double.TYPE) { return 1; } else if (aType == Float.TYPE) { return 2; } else if (aType == Long.TYPE) { return 3; } else if (aType == Integer.TYPE) { return 4; } else if (aType == Short.TYPE) { return 5; } else if (aType == Character.TYPE) { return 6; } else if (aType == Byte.TYPE) { return 7; } else if (aType == Boolean.TYPE) { return CONVERSION_NONE; } else { return 8; } } private static int getJSTypeCode(Object value) { if (value == null) { return JSTYPE_NULL; } else if (value == Undefined.instance) { return JSTYPE_UNDEFINED; } else if (value instanceof String) { return JSTYPE_STRING; } else if (value instanceof Number) { return JSTYPE_NUMBER; } else if (value instanceof Boolean) { return JSTYPE_BOOLEAN; } else if (value instanceof Scriptable) { if (value instanceof NativeJavaClass) { return JSTYPE_JAVA_CLASS; } else if (value instanceof NativeJavaArray) { return JSTYPE_JAVA_ARRAY; } else if (value instanceof Wrapper) { return JSTYPE_JAVA_OBJECT; } else { return JSTYPE_OBJECT; } } else if (value instanceof Class) { return JSTYPE_JAVA_CLASS; } else { Class valueClass = value.getClass(); if (valueClass.isArray()) { return JSTYPE_JAVA_ARRAY; } else { return JSTYPE_JAVA_OBJECT; } } } /** * Not intended for public use. Callers should use the * public API Context.toType. * @deprecated as of 1.5 Release 4 * @see org.mozilla.javascript.Context#jsToJava(Object, Class) */ public static Object coerceType(Class type, Object value) { return coerceTypeImpl(type, value); } /** * Type-munging for field setting and method invocation. * Conforms to LC3 specification */ static Object coerceTypeImpl(Class type, Object value) { if (value != null && value.getClass() == type) { return value; } switch (getJSTypeCode(value)) { case JSTYPE_NULL: // raise error if type.isPrimitive() if (type.isPrimitive()) { reportConversionError(value, type); } return null; case JSTYPE_UNDEFINED: if (type == ScriptRuntime.StringClass || type == ScriptRuntime.ObjectClass) { return "undefined"; } else { reportConversionError("undefined", type); } break; case JSTYPE_BOOLEAN: // Under LC3, only JS Booleans can be coerced into a Boolean value if (type == Boolean.TYPE || type == ScriptRuntime.BooleanClass || type == ScriptRuntime.ObjectClass) { return value; } else if (type == ScriptRuntime.StringClass) { return value.toString(); } else { reportConversionError(value, type); } break; case JSTYPE_NUMBER: if (type == ScriptRuntime.StringClass) { return ScriptRuntime.toString(value); } else if (type == ScriptRuntime.ObjectClass) { return coerceToNumber(Double.TYPE, value); } else if ((type.isPrimitive() && type != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(type)) { return coerceToNumber(type, value); } else { reportConversionError(value, type); } break; case JSTYPE_STRING: if (type == ScriptRuntime.StringClass || type.isInstance(value)) { return value; } else if (type == Character.TYPE || type == ScriptRuntime.CharacterClass) { // Special case for converting a single char string to a // character // Placed here because it applies *only* to JS strings, // not other JS objects converted to strings if (((String)value).length() == 1) { return new Character(((String)value).charAt(0)); } else { return coerceToNumber(type, value); } } else if ((type.isPrimitive() && type != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(type)) { return coerceToNumber(type, value); } else { reportConversionError(value, type); } break; case JSTYPE_JAVA_CLASS: if (value instanceof Wrapper) { value = ((Wrapper)value).unwrap(); } if (type == ScriptRuntime.ClassClass || type == ScriptRuntime.ObjectClass) { return value; } else if (type == ScriptRuntime.StringClass) { return value.toString(); } else { reportConversionError(value, type); } break; case JSTYPE_JAVA_OBJECT: case JSTYPE_JAVA_ARRAY: if (value instanceof Wrapper) { value = ((Wrapper)value).unwrap(); } if (type.isPrimitive()) { if (type == Boolean.TYPE) { reportConversionError(value, type); } return coerceToNumber(type, value); } else { if (type == ScriptRuntime.StringClass) { return value.toString(); } else { if (type.isInstance(value)) { return value; } else { reportConversionError(value, type); } } } break; case JSTYPE_OBJECT: if (type == ScriptRuntime.StringClass) { return ScriptRuntime.toString(value); } else if (type.isPrimitive()) { if (type == Boolean.TYPE) { reportConversionError(value, type); } return coerceToNumber(type, value); } else if (type.isInstance(value)) { return value; } else if (type == ScriptRuntime.DateClass && value instanceof NativeDate) { double time = ((NativeDate)value).getJSTimeValue(); // XXX: This will replace NaN by 0 return new Date((long)time); } else if (type.isArray() && value instanceof NativeArray) { // Make a new java array, and coerce the JS array components // to the target (component) type. NativeArray array = (NativeArray) value; long length = array.getLength(); Class arrayType = type.getComponentType(); Object Result = Array.newInstance(arrayType, (int)length); for (int i = 0 ; i < length ; ++i) { try { Array.set(Result, i, coerceType(arrayType, array.get(i, array))); } catch (EvaluatorException ee) { reportConversionError(value, type); } } return Result; } else if (value instanceof Wrapper) { value = ((Wrapper)value).unwrap(); if (type.isInstance(value)) return value; reportConversionError(value, type); } else if (type.isInterface() && value instanceof Callable) { // Try to use function as implementation of Java interface. // // XXX: Curently only instances of ScriptableObject are // supported since the resulting interface proxies should // be reused next time conversion is made and generic // Callable has no storage for it. Weak references can // address it but for now use this restriction. if (value instanceof ScriptableObject) { ScriptableObject so = (ScriptableObject)value; Object key = Kit.makeHashKeyFromPair( COERCED_INTERFACE_KEY, type); Object old = so.getAssociatedValue(key); if (old != null) { // Function was already wrapped return old; } Context cx = Context.getContext(); Object glue = InterfaceAdapter.create(cx, type, (Callable)value); // Store for later retrival glue = so.associateValue(key, glue); return glue; } reportConversionError(value, type); } else { reportConversionError(value, type); } break; } return value; } private static Object coerceToNumber(Class type, Object value) { Class valueClass = value.getClass(); // Character if (type == Character.TYPE || type == ScriptRuntime.CharacterClass) { if (valueClass == ScriptRuntime.CharacterClass) { return value; } return new Character((char)toInteger(value, ScriptRuntime.CharacterClass, Character.MIN_VALUE, Character.MAX_VALUE)); } // Double, Float if (type == ScriptRuntime.ObjectClass || type == ScriptRuntime.DoubleClass || type == Double.TYPE) { return valueClass == ScriptRuntime.DoubleClass ? value : new Double(toDouble(value)); } if (type == ScriptRuntime.FloatClass || type == Float.TYPE) { if (valueClass == ScriptRuntime.FloatClass) { return value; } else { double number = toDouble(value); if (Double.isInfinite(number) || Double.isNaN(number) || number == 0.0) { return new Float((float)number); } else { double absNumber = Math.abs(number); if (absNumber < Float.MIN_VALUE) { return new Float((number > 0.0) ? +0.0 : -0.0); } else if (absNumber > Float.MAX_VALUE) { return new Float((number > 0.0) ? Float.POSITIVE_INFINITY : Float.NEGATIVE_INFINITY); } else { return new Float((float)number); } } } } // Integer, Long, Short, Byte if (type == ScriptRuntime.IntegerClass || type == Integer.TYPE) { if (valueClass == ScriptRuntime.IntegerClass) { return value; } else { return new Integer((int)toInteger(value, ScriptRuntime.IntegerClass, Integer.MIN_VALUE, Integer.MAX_VALUE)); } } if (type == ScriptRuntime.LongClass || type == Long.TYPE) { if (valueClass == ScriptRuntime.LongClass) { return value; } else { /* Long values cannot be expressed exactly in doubles. * We thus use the largest and smallest double value that * has a value expressible as a long value. We build these * numerical values from their hexidecimal representations * to avoid any problems caused by attempting to parse a * decimal representation. */ final double max = Double.longBitsToDouble(0x43dfffffffffffffL); final double min = Double.longBitsToDouble(0xc3e0000000000000L); return new Long(toInteger(value, ScriptRuntime.LongClass, min, max)); } } if (type == ScriptRuntime.ShortClass || type == Short.TYPE) { if (valueClass == ScriptRuntime.ShortClass) { return value; } else { return new Short((short)toInteger(value, ScriptRuntime.ShortClass, Short.MIN_VALUE, Short.MAX_VALUE)); } } if (type == ScriptRuntime.ByteClass || type == Byte.TYPE) { if (valueClass == ScriptRuntime.ByteClass) { return value; } else { return new Byte((byte)toInteger(value, ScriptRuntime.ByteClass, Byte.MIN_VALUE, Byte.MAX_VALUE)); } } return new Double(toDouble(value)); } private static double toDouble(Object value) { if (value instanceof Number) { return ((Number)value).doubleValue(); } else if (value instanceof String) { return ScriptRuntime.toNumber((String)value); } else if (value instanceof Scriptable) { if (value instanceof Wrapper) { // XXX: optimize tail-recursion? return toDouble(((Wrapper)value).unwrap()); } else { return ScriptRuntime.toNumber(value); } } else { Method meth; try { meth = value.getClass().getMethod("doubleValue", (Class [])null); } catch (NoSuchMethodException e) { meth = null; } catch (SecurityException e) { meth = null; } if (meth != null) { try { return ((Number)meth.invoke(value, (Object [])null)).doubleValue(); } catch (IllegalAccessException e) { // XXX: ignore, or error message? reportConversionError(value, Double.TYPE); } catch (InvocationTargetException e) { // XXX: ignore, or error message? reportConversionError(value, Double.TYPE); } } return ScriptRuntime.toNumber(value.toString()); } } private static long toInteger(Object value, Class type, double min, double max) { double d = toDouble(value); if (Double.isInfinite(d) || Double.isNaN(d)) { // Convert to string first, for more readable message reportConversionError(ScriptRuntime.toString(value), type); } if (d > 0.0) { d = Math.floor(d); } else { d = Math.ceil(d); } if (d < min || d > max) { // Convert to string first, for more readable message reportConversionError(ScriptRuntime.toString(value), type); } return (long)d; } static void reportConversionError(Object value, Class type) { // It uses String.valueOf(value), not value.toString() since // value can be null, bug 282447. throw Context.reportRuntimeError2( "msg.conversion.not.allowed", String.valueOf(value), JavaMembers.javaSignature(type)); } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeBoolean(isAdapter); if (isAdapter) { if (adapter_writeAdapterObject == null) { throw new IOException(); } Object[] args = { javaObject, out }; try { adapter_writeAdapterObject.invoke(null, args); } catch (Exception ex) { throw new IOException(); } } else { out.writeObject(javaObject); } if (staticType != null) { out.writeObject(staticType.getClass().getName()); } else { out.writeObject(null); } } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); isAdapter = in.readBoolean(); if (isAdapter) { if (adapter_readAdapterObject == null) throw new ClassNotFoundException(); Object[] args = { this, in }; try { javaObject = adapter_readAdapterObject.invoke(null, args); } catch (Exception ex) { throw new IOException(); } } else { javaObject = in.readObject(); } String className = (String)in.readObject(); if (className != null) { staticType = Class.forName(className); } else { staticType = null; } initMembers(); } /** * The prototype of this object. */ protected Scriptable prototype; /** * The parent scope of this object. */ protected Scriptable parent; protected transient Object javaObject; protected transient Class staticType; protected transient JavaMembers members; private transient Hashtable fieldAndMethods; private transient boolean isAdapter; private static final Object COERCED_INTERFACE_KEY = new Object(); private static Method adapter_writeAdapterObject; private static Method adapter_readAdapterObject; static { // Reflection in java is verbose Class[] sig2 = new Class[2]; Class cl = Kit.classOrNull("org.mozilla.javascript.JavaAdapter"); if (cl != null) { try { sig2[0] = ScriptRuntime.ObjectClass; sig2[1] = Kit.classOrNull("java.io.ObjectOutputStream"); adapter_writeAdapterObject = cl.getMethod("writeAdapterObject", sig2); sig2[0] = ScriptRuntime.ScriptableClass; sig2[1] = Kit.classOrNull("java.io.ObjectInputStream"); adapter_readAdapterObject = cl.getMethod("readAdapterObject", sig2); } catch (Exception ex) { adapter_writeAdapterObject = null; adapter_readAdapterObject = null; } } } }