/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.shell; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.dev.util.TypeInfo; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; /** * Glue layer that performs GWT-specific operations on JsValues. Used to isolate * HostedModeExceptions/etc from JsValue code */ public final class JsValueGlue { public static final String HOSTED_MODE_REFERENCE = "hostedModeReference"; public static final String JSO_CLASS = "com.google.gwt.core.client.JavaScriptObject"; public static final String JSO_IMPL_CLASS = "com.google.gwt.core.client.JavaScriptObject$"; /** * Create a JavaScriptObject instance referring to this JavaScript object. * * @param classLoader the classLoader to create from * @return the constructed JavaScriptObject */ public static Object createJavaScriptObject(JsValue value, CompilingClassLoader classLoader) { Throwable caught; try { // See if there's already a wrapper object (assures identity comparison). Object jso = classLoader.getCachedJso(value.getJavaScriptObjectPointer()); if (jso != null) { return jso; } // Instantiate the JSO class. Class<?> jsoType = Class.forName(JSO_IMPL_CLASS, true, classLoader); Constructor<?> ctor = jsoType.getDeclaredConstructor(); ctor.setAccessible(true); jso = ctor.newInstance(); // Set the reference field to this JsValue using reflection. Field referenceField = jsoType.getField(HOSTED_MODE_REFERENCE); referenceField.set(jso, value); classLoader.putCachedJso(value.getJavaScriptObjectPointer(), jso); return jso; } catch (InstantiationException e) { caught = e; } catch (IllegalAccessException e) { caught = e; } catch (SecurityException e) { caught = e; } catch (NoSuchMethodException e) { caught = e; } catch (IllegalArgumentException e) { caught = e; } catch (InvocationTargetException e) { caught = e; } catch (ClassNotFoundException e) { caught = e; } catch (NoSuchFieldException e) { caught = e; } throw new RuntimeException("Error creating JavaScript object", caught); } /** * Return an object containing the value JavaScript object as a specified * type. * * @param value the JavaScript value * @param type expected type of the returned object * @param msgPrefix a prefix for error/warning messages * @return the object reference * @throws com.google.gwt.dev.shell.HostedModeException if the JavaScript * object is not assignable to the supplied type. */ @SuppressWarnings("unchecked") public static <T> T get(JsValue value, CompilingClassLoader cl, Class<T> type, String msgPrefix) { if (type.isPrimitive()) { if (type == Boolean.TYPE) { if (!value.isBoolean()) { throw new HostedModeException(msgPrefix + ": JS value of type " + value.getTypeString() + ", expected boolean"); } return (T) Boolean.valueOf(value.getBoolean()); } else if (type == Byte.TYPE) { return (T) Byte.valueOf((byte) getIntRange(value, Byte.MIN_VALUE, Byte.MAX_VALUE, "byte", msgPrefix)); } else if (type == Character.TYPE) { return (T) Character.valueOf((char) getIntRange(value, Character.MIN_VALUE, Character.MAX_VALUE, "char", msgPrefix)); } else if (type == Double.TYPE) { if (!value.isNumber()) { throw new HostedModeException(msgPrefix + ": JS value of type " + value.getTypeString() + ", expected double"); } return (T) Double.valueOf(value.getNumber()); } else if (type == Float.TYPE) { if (!value.isNumber()) { throw new HostedModeException(msgPrefix + ": JS value of type " + value.getTypeString() + ", expected float"); } double doubleVal = value.getNumber(); // Check for small changes near MIN_VALUE and replace with the // actual end point value, in case it is being used as a sentinel // value. This test works by the subtraction result rounding off to // zero if the delta is not representable in a float. // TODO(jat): add similar test for MAX_VALUE if we have a JS // platform that alters the value while converting to/from strings. if ((float) (doubleVal - Float.MIN_VALUE) == 0.0f) { doubleVal = Float.MIN_VALUE; } float floatVal = (float) doubleVal; if (Float.isInfinite(floatVal) && !Double.isInfinite(doubleVal)) { // in this case we had overflow from the double value which was // outside the range of supported float values, and the cast // converted it to infinity. Since this lost data, we treat this // as an error in hosted mode. throw new HostedModeException(msgPrefix + ": JS value " + doubleVal + " out of range for a float"); } return (T) Float.valueOf(floatVal); } else if (type == Integer.TYPE) { return (T) Integer.valueOf(getIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, "int", msgPrefix)); } else if (type == Long.TYPE) { if (!value.isWrappedJavaObject()) { throw new HostedModeException(msgPrefix + ": JS value of type " + value.getTypeString() + ", expected Java long"); } JavaLong javaLong = (JavaLong) value.getWrappedJavaObject(); return (T) Long.valueOf(javaLong.longValue()); } else if (type == Short.TYPE) { return (T) Short.valueOf((short) getIntRange(value, Short.MIN_VALUE, Short.MAX_VALUE, "short", msgPrefix)); } } if (value.isNull() || value.isUndefined()) { return null; } if (value.isWrappedJavaObject()) { return type.cast(value.getWrappedJavaObject()); } if (value.isString()) { return type.cast(value.getString()); } if (value.isJavaScriptObject()) { return type.cast(createJavaScriptObject(value, cl)); } // Just don't know what do to with this. /* * TODO (amitmanjhi): does throwing a HostedModeException here and catching * a RuntimeException in user test * com.google.gwt.dev.jjs.test.HostedTest::testObjectReturns() make sense */ throw new IllegalArgumentException(msgPrefix + ": JS value of type " + value.getTypeString() + ", expected " + TypeInfo.getSourceRepresentation(type)); } /** * Set the underlying value. * * @param value JsValue to set * @param type static type of the object * @param obj the object to store in the JS value */ public static void set(JsValue value, CompilingClassLoader cl, Class<?> type, Object obj) { if (type.isPrimitive()) { if (type == Boolean.TYPE) { value.setBoolean(((Boolean) obj).booleanValue()); } else if (type == Byte.TYPE) { value.setInt(((Byte) obj).byteValue()); } else if (type == Character.TYPE) { value.setInt(((Character) obj).charValue()); } else if (type == Double.TYPE) { value.setDouble(((Double) obj).doubleValue()); } else if (type == Float.TYPE) { value.setDouble(((Float) obj).floatValue()); } else if (type == Integer.TYPE) { value.setInt(((Integer) obj).intValue()); } else if (type == Long.TYPE) { long longVal = ((Long) obj).longValue(); value.setWrappedJavaObject(cl, new JavaLong(longVal)); } else if (type == Short.TYPE) { value.setInt(((Short) obj).shortValue()); } else if (type == Void.TYPE) { value.setUndefined(); } else { throw new HostedModeException("Cannot marshal primitive type " + type); } } else if (obj == null) { value.setNull(); } else { // not a boxed primitive try { Class<?> jsoType = Class.forName(JSO_IMPL_CLASS, false, cl); if (jsoType == obj.getClass()) { JsValue jsObject = getUnderlyingObject(obj); value.setValue(jsObject); return; } } catch (ClassNotFoundException e) { // Ignore the exception, if we can't find the class then obviously we // don't have to worry about o being one } // Fall through case: Object. if (!type.isInstance(obj)) { throw new HostedModeException("object is of type " + obj.getClass().getName() + ", expected " + type.getName()); } if (obj instanceof String) { value.setString((String) obj); } else { value.setWrappedJavaObject(cl, obj); } } } private static int getIntRange(JsValue value, int low, int high, String typeName, String msgPrefix) { int intVal; if (value.isInt()) { intVal = value.getInt(); if (intVal < low || intVal > high) { throw new HostedModeException(msgPrefix + ": JS int value " + intVal + " out of range for a " + typeName); } } else if (value.isNumber()) { double doubleVal = value.getNumber(); if (doubleVal < low || doubleVal > high) { throw new HostedModeException(msgPrefix + ": JS double value " + doubleVal + " out of range for a " + typeName); } intVal = (int) doubleVal; if (intVal != doubleVal) { ModuleSpace.getLogger().log(TreeLogger.WARN, msgPrefix + ": Rounding double (" + doubleVal + ") to int for " + typeName, null); } } else { throw new HostedModeException(msgPrefix + ": JS value of type " + value.getTypeString() + ", expected " + typeName); } return intVal; } /** * Returns the underlying JsValue from a JavaScriptObject instance. * * The tricky part is that it is in a different ClassLoader so therefore can't * be specified directly. The type is specified as Object, and reflection is * used to retrieve the reference field. * * @param jso the instance of JavaScriptObject to retrieve the JsValue from. * @return the JsValue representing the JavaScript object */ private static JsValue getUnderlyingObject(Object jso) { Throwable caught; try { Field referenceField = jso.getClass().getField(HOSTED_MODE_REFERENCE); referenceField.setAccessible(true); return (JsValue) referenceField.get(jso); } catch (IllegalAccessException e) { caught = e; } catch (SecurityException e) { caught = e; } catch (NoSuchFieldException e) { caught = e; } throw new RuntimeException("Error reading " + HOSTED_MODE_REFERENCE, caught); } private JsValueGlue() { } }