/* * Copyright 2011 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.core.ext.debug; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * Provides facilities for debuggers to call methods on * {@link com.google.gwt.core.client.JavaScriptObject JavaScriptObjects}. * <p/> * Because devmode does extensive rewriting of JSO bytecode, debuggers can't * figure out how to evaluate JSO method calls. This class can be used directly * by users to evaluate JSO methods in their debuggers. Additionally, debuggers * with GWT support use this class to transparently evaluate JSO expressions in * breakpoints, watch windows, etc. * <p> * Example uses: * <code><pre> * JsoEval.call(Element.class, myElement, "getAbsoluteTop"); * JsoEval.call(Node.class, myNode, "cloneNode", Boolean.TRUE); * JsoEval.call(Element.class, element.getFirstChildElement(), "setPropertyString", "phase", * "gamma"); * </pre></code> * @noinspection UnusedDeclaration */ public class JsoEval { /* TODO: Error messages generated from JsoEval are reported with mangled * method names and signatures instead of original source code values. * We could de-mangle the names for the errors, but it really only matters * for users who don't have IDE support. */ // TODO: Update the wiki doc to include a better description of JSO transformations and reference // it from here. private static Map<Class,Class> boxedTypeForPrimitiveType = new HashMap<Class,Class>(8); private static Map<Class,Class> primitiveTypeForBoxedType = new HashMap<Class,Class>(8); private static final String JSO_IMPL_CLASS = "com.google.gwt.core.client.JavaScriptObject$"; static { boxedTypeForPrimitiveType.put(boolean.class, Boolean.class); boxedTypeForPrimitiveType.put(byte.class, Byte.class); boxedTypeForPrimitiveType.put(short.class, Short.class); boxedTypeForPrimitiveType.put(char.class, Character.class); boxedTypeForPrimitiveType.put(int.class, Integer.class); boxedTypeForPrimitiveType.put(float.class, Float.class); boxedTypeForPrimitiveType.put(long.class, Long.class); boxedTypeForPrimitiveType.put(double.class, Double.class); for (Map.Entry<Class,Class> entry : boxedTypeForPrimitiveType.entrySet()) { primitiveTypeForBoxedType.put(entry.getValue(), entry.getKey()); } } /** * Reflectively invokes a method on a JavaScriptObject. * * @param klass Either a class of type JavaScriptObject or an interface * implemented by a JavaScriptObject. The class must contain the method to * be invoked. * @param obj The JavaScriptObject to invoke the method on. Must be null if * the method is static. Must be not-null if the method is not static * @param methodName The name of the method * @param types The types of the arguments * @param args The values of the arguments * * @return The result of the method invocation or the failure as a String */ public static Object call(Class klass, Object obj, String methodName, Class[] types, Object... args) { try { return callEx(klass, obj, methodName, types, args); } catch (Exception e) { return toString(e); } } /** * A convenience form of * {@link #call(Class, Object, String, Class[], Object...)} for use directly * by users in a debugger. This method guesses at the types of the method * based on the values of {@code args}. * * @return The result of the method invocation or the failure as a String */ public static Object call(Class klass, Object obj, String methodName, Object... args) { try { return callEx(klass, obj, methodName, args); } catch (Exception e) { return toString(e); } } /** * Reflectively invokes a method on a JavaScriptObject. * * @param klass Either a class of type JavaScriptObject or an interface * implemented by a JavaScriptObject. The class must contain the method to * be invoked. * @param obj The JavaScriptObject to invoke the method on. Must be null if * the method is static. Must be not-null if the method is not static * @param methodName The name of the method * @param types The types of the arguments * @param args The values of the arguments * * @return The result of the method invocation */ public static Object callEx(Class klass, Object obj, String methodName, Class[] types, Object... args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { return invoke(klass, obj, getJsoMethod(klass, obj, methodName, types), args); } /** * A convenience form of * {@link #call(Class, Object, String, Class[], Object...)} for use directly * by users in a debugger. This method guesses at the types of the method * based on the values of {@code args}. */ public static Object callEx(Class klass, Object obj, String methodName, Object... args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { if (args == null) { // A single-argument varargs null can come in unboxed args = new Object[]{null}; } if (obj != null) { if (!obj.getClass().getName().equals(JSO_IMPL_CLASS)) { throw new RuntimeException(obj + " is not a JavaScriptObject."); } } // First check java.lang.Object methods for exact matches Method[] methods = Object.class.getMethods(); nextMethod: for (Method m : methods) { if (m.getName().equals(methodName)) { Class[] types = m.getParameterTypes(); if (types.length != args.length) { continue; } for (int i = 0, j = 0; i < args.length; ++i, ++j) { if (!isAssignable(types[i], args[j])) { continue nextMethod; } } return m.invoke(obj, args); } } ClassLoader ccl = getCompilingClassLoader(klass, obj); boolean isJso = isJso(ccl, klass); boolean isStaticifiedDispatch = isJso && obj != null; int actualNumArgs = isStaticifiedDispatch ? args.length + 1 : args.length; ArrayList<Method> matchingMethods = new ArrayList<Method>(Arrays.asList( isJso ? getSisterJsoImpl(klass, ccl).getMethods() : getJsoImplClass(ccl).getMethods())); String mangledMethodName = mangleMethod(klass, methodName, isJso, isStaticifiedDispatch); // Filter the methods in multiple passes to give better error messages. for (Iterator<Method> it = matchingMethods.iterator(); it.hasNext();) { Method m = it.next(); if (!m.getName().equalsIgnoreCase(mangledMethodName)) { it.remove(); } } if (matchingMethods.isEmpty()) { throw new RuntimeException( "No methods by the name, " + methodName + ", could be found in " + klass); } ArrayList<Method> candidates = new ArrayList<Method>(matchingMethods); for (Iterator<Method> it = matchingMethods.iterator(); it.hasNext();) { Method m = it.next(); if (m.getParameterTypes().length != actualNumArgs) { it.remove(); } } if (matchingMethods.isEmpty()) { throw new RuntimeException( "No methods by the name, " + methodName + ", in " + klass + " accept " + args.length + " parameters. Candidates are:\n" + candidates); } candidates = new ArrayList<Method>(matchingMethods); nextMethod: for (Iterator<Method> it = matchingMethods.iterator(); it.hasNext();) { Method m = it.next(); Class[] methodTypes = m.getParameterTypes(); for (int i = isStaticifiedDispatch ? 1 : 0, j = 0; i < methodTypes.length; ++i, ++j) { if (!isAssignable(methodTypes[i], args[j])) { it.remove(); continue nextMethod; } } } if (matchingMethods.isEmpty()) { throw new RuntimeException( "No methods accepting " + Arrays.asList(args) + " were found for, " + methodName + ", in " + klass + ". Candidates:\n" + candidates); } candidates = new ArrayList<Method>(matchingMethods); if (matchingMethods.size() > 1) { // Try to filter by exact name on the crazy off chance there are two // methods by same name but different case. for (Iterator<Method> it = matchingMethods.iterator(); it.hasNext();) { Method m = it.next(); if (!m.getName().equals(mangledMethodName)) { it.remove(); } } } if (matchingMethods.isEmpty()) { throw new RuntimeException( "Multiple methods with a case-insensitive match were found for, " + methodName + ", in " + klass + ". Candidates:\n" + candidates); } if (matchingMethods.size() > 1) { throw new RuntimeException( "Found more than one matching method. Please specify the types of the parameters. " + "Candidates:\n" + matchingMethods); } return invoke(klass, obj, matchingMethods.get(0), args); } /** * Reflectively invokes a static method on a JavaScriptObject. Has the same * effect as calling {@link #call(Class, Object, String, Class[], Object...) * call(klass, null, methodName, types, args)} * * @return The result of the method invocation or the failure as a String */ public static Object callStatic(Class klass, String methodName, Class[] types, Object... args) { try { return callStaticEx(klass, methodName, types, args); } catch (Exception e) { return toString(e); } } /** * Reflectively invokes a static method on a JavaScriptObject. Has the same * effect as calling {@link #call(Class, Object, String, Class[], Object...) * call(klass, null, methodName, types, args)} */ public static Object callStaticEx(Class klass, String methodName, Class[] types, Object... args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { return call(klass, null, methodName, types, args); } /** * Try to find the CompilingClassLoader. This can fail if<ol> * <li> the user provides an object that isn't a JSO or * <li>the user provides a null JSO and a Class that wasn't loaded by the * CompilingClassLoader * </ol> * I don't have any great solutions for that scenario. */ private static ClassLoader getCompilingClassLoader(Class klass, Object obj) { ClassLoader ccl; if (obj != null) { ccl = obj.getClass().getClassLoader(); } else { // try passed in class ccl = klass.getClassLoader(); } if (ccl == null || !ccl.getClass().getName().equals("com.google.gwt.dev.shell.CompilingClassLoader")) { if (obj != null) { throw new RuntimeException( "The object, " + obj + ", does not appear to be a JavaScriptObject or an interface " + "implemented by a JavaScriptObject. GWT could not find a CompilingClassLoader " + "for it."); } else { throw new RuntimeException( "The class, " + klass + ", does not appear to be a JavaScriptObject or an interface " + "implemented by a JavaScriptObject. GWT could not find a CompilingClassLoader " + " for it."); } } return ccl; } /** * Returns the class for {@code JavaScriptObject}. We need the version which * is loaded by a specific CompilingClassLoader. */ private static Class getJsoClass(ClassLoader cl) { try { return Class.forName("com.google.gwt.core.client.JavaScriptObject", false, cl); } catch (ClassNotFoundException e) { throw new RuntimeException("Failed to find JavaScriptObject", e); } } /** * Returns the class for {@code JavaScriptObject$}. We need the version which * is loaded by a specific CompilingClassLoader. */ private static Class getJsoImplClass(ClassLoader cl) { try { return Class.forName(JSO_IMPL_CLASS, false, cl); } catch (ClassNotFoundException e) { throw new RuntimeException("Failed to find " + JSO_IMPL_CLASS, e); } } private static Method getJsoMethod(Class klass, Object obj, String methodName, Class[] types) throws ClassNotFoundException, NoSuchMethodException { if (obj != null) { if (!obj.getClass().getName().equals(JSO_IMPL_CLASS)) { throw new RuntimeException(obj + " is not a JavaScriptObject."); } } // First see if it's a method inherited from java.lang.Object Method[] methods = Object.class.getMethods(); for (Method m : methods) { if (m.getName().equals(methodName) && Arrays.equals(m.getParameterTypes(), types)) { return m; } } ClassLoader ccl = getCompilingClassLoader(klass, obj); boolean isJso = isJso(ccl, klass); boolean isStaticifiedDispatch = isJso && obj != null; String mangledMethod = mangleMethod(klass, methodName, isJso, isStaticifiedDispatch); if (!isJso) { // If this is interface dispatch, then the method lives on // JavaScriptObject$ and is mangled so that it doesn't conflict with any // other classes. Class jsoImplClass = getJsoImplClass(ccl); try { return jsoImplClass.getMethod(mangledMethod, types); } catch (NoSuchMethodException e) { throw new RuntimeException("Unable to find the interface method, " + methodName + ". Is there a JSO that implements it?", e); } } // All other methods lives on the impl subclass of JavaScriptObject$, // and have been rewritten to be static dispatch. Class jsoImplSubclass = getSisterJsoImpl(klass, ccl); if (obj != null) { // If this is an instance method, we need to insert obj as the "this" ref // in the args Class[] newTypes = new Class[types.length + 1]; newTypes[0] = klass; System.arraycopy(types, 0, newTypes, 1, types.length); types = newTypes; } return jsoImplSubclass.getMethod(mangledMethod, types); } private static Class<?> getSisterJsoImpl(Class klass, ClassLoader ccl) throws ClassNotFoundException { return Class.forName(klass.getName() + '$', false, ccl); } private static Object invoke(Class klass, Object obj, Method m, Object... args) throws InvocationTargetException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException { if (args == null) { // A single-argument varargs null can come in unboxed args = new Object[]{null}; } ClassLoader ccl = getCompilingClassLoader(klass, obj); if (!isJso(ccl, klass)) { // Calling through a non-JSO interface - normal instance dispatch. Object result = m.invoke(obj, args); return m.getReturnType() == void.class ? "[success]" : result; } // All other methods lives on the impl subclass of JavaScriptObject$, // and have been rewritten to be static dispatch. if (obj != null) { // If this is an instance method, we need to insert obj as the "this" // ref in the args Object[] newArgs = new Object[args.length + 1]; newArgs[0] = obj; System.arraycopy(args, 0, newArgs, 1, args.length); args = newArgs; } Object result = m.invoke(obj, args); return m.getReturnType() == void.class ? "[success]" : result; } private static boolean isAssignable(Class type, Object value) { if (value == null) { return !type.isPrimitive(); } Class valueType = value.getClass(); if (type.isAssignableFrom(valueType)) { return true; } if (boxedTypeForPrimitiveType.get(valueType) == type || primitiveTypeForBoxedType.get(valueType) == type) { return true; } return false; } private static boolean isJso(ClassLoader ccl, Class klass) { return getJsoClass(ccl).isAssignableFrom(klass); } private static String mangleMethod(Class klass, String methodName, boolean isJso, boolean isVirtual) { // If this is interface dispatch from a non-JSO, then the method lives on // JavaScriptObject$ and is mangled with the fully qualified class name so // that it doesn't conflict with methods from other classes. Otherwise // virtual dispatch is re-written to static dispatch, and a '$' is // appended to the name of the method. return isJso ? isVirtual ? methodName + '$' : methodName : klass.getName().replace('.', '_') + '_' + methodName; } private static String toString(Exception e) { StringWriter sw = new StringWriter(); PrintWriter w = new PrintWriter(sw); e.printStackTrace(w); w.close(); return sw.toString(); } private JsoEval() { } }