blob: c6f35856c95daaa2619b884fe97dd2c9ca638af4 [file] [log] [blame]
/*
* 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>
*/
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() {
}
}