Revision: 10300
Author:   [email protected]
Date:     Wed Jun  8 11:39:45 2011
Log: Adds support for runtime evaluation of JavaScriptObject methods from a debugger. Primarily intended as support API for debuggers, but developers can also use it directly in a debugger (for example, in watch windows or breakpoint expressions).

Review at http://gwt-code-reviews.appspot.com/1453808

Review by: [email protected]
http://code.google.com/p/google-web-toolkit/source/detail?r=10300

Added:
 /trunk/dev/core/src/com/google/gwt/core/ext/debug
 /trunk/dev/core/src/com/google/gwt/core/ext/debug/JsoEval.java
Modified:
 /trunk/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java

=======================================
--- /dev/null
+++ /trunk/dev/core/src/com/google/gwt/core/ext/debug/JsoEval.java Wed Jun 8 11:39:45 2011
@@ -0,0 +1,471 @@
+/*
+ * 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() {
+  }
+}
=======================================
--- /trunk/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java Tue Apr 26 08:02:24 2011 +++ /trunk/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java Wed Jun 8 11:39:45 2011
@@ -1038,6 +1038,13 @@
           new NullPointerException());
     }

+    if (className.equals("com.google.gwt.core.ext.debug.JsoEval")) {
+      // In addition to the system ClassLoader, we let JsoEval be available
+      // from this CompilingClassLoader in case that's where the debugger
+      // happens to look.
+      return ClassLoader.getSystemClassLoader().loadClass(className);
+    }
+
     if (scriptOnlyClasses.contains(className)) {
       // Allow the child ClassLoader to handle this
       throw new ClassNotFoundException();

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to