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