/*
* 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.inject.rebind.util;
import static com.google.gwt.inject.rebind.util.SourceWriteUtil.join;
import com.google.gwt.inject.rebind.reflect.MethodLiteral;
import com.google.gwt.inject.rebind.reflect.NoSourceNameException;
import com.google.gwt.inject.rebind.reflect.ReflectUtil;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Utility code to create method and constructor calls.
*/
public class MethodCallUtil {
/**
* Creates a constructor injecting method and returns a string that invokes
* the new method. The new method returns the constructed object.
*
* @param constructor constructor to call
* @param nameGenerator NameGenerator to be used for ensuring method name uniqueness
* @param methodsOutput a list where all new methods created by this
* call are added
* @return source snippet calling the generated method
*/
public SourceSnippet createConstructorInjection(
MethodLiteral<?, Constructor<?>> constructor, NameGenerator nameGenerator,
List<InjectorMethod> methodsOutput) throws NoSourceNameException {
return createMethodCallWithInjection(constructor, null, nameGenerator, methodsOutput);
}
/**
* Creates a method that calls the passed method, injecting its parameters
* using getters, and returns a string that invokes the new method. The new
* method returns the passed method's return value, if any. If a method
* without parameters is provided, that method will be called and no
* parameters will be passed.
*
* @param method method to call (can be constructor)
* @param invokeeName expression that evaluates to the object on which
* the method is to be called. If null the method will be called
* in the current scope.
* @param nameGenerator NameGenerator to be used for ensuring method name uniqueness
* @param methodsOutput a list where all new methods created by this
* call are added
* @return source snippet calling the generated method
*/
public SourceSnippet createMethodCallWithInjection(MethodLiteral<?, ?> method,
String invokeeName, NameGenerator nameGenerator, List<InjectorMethod> methodsOutput)
throws NoSourceNameException {
String[] params = new String[method.getParameterTypes().size()];
return createMethodCallWithInjection(method, invokeeName, params, nameGenerator,
methodsOutput);
}
/**
* Creates a method that calls the passed method, injecting its parameters
* using getters as necessary, and returns a string that invokes the new
* method. The new method returns the passed method's return value, if any.
* If a method without parameters is provided, that method will be called and
* no parameters will be passed. If the passed method declared any checked
* exceptions, the generated method will catch and rethrow those as
* {@link com.google.gwt.inject.client.CreationException}.
*
* @param method method to call (can be constructor)
* @param invokeeName expression that evaluates to the object on which
* the method is to be called. If null the method will be called
* in the current scope.
* @param parameterNames array with parameter names that can replace getter
* methods (usually used to fetch injected values) in the returned
* string. The array length must match the number of method
* parameters. A {@code null} value denotes that the getter method
* should be used.
* @param nameGenerator NameGenerator to use for ensuring method name uniqueness
* @param methodsOutput a list where all new methods created by this
* call are added
* @return string calling the generated method
*/
public SourceSnippet createMethodCallWithInjection(MethodLiteral<?, ?> method,
String invokeeName, String[] parameterNames, NameGenerator nameGenerator,
List<InjectorMethod> methodsOutput) throws NoSourceNameException {
boolean hasInvokee = invokeeName != null;
boolean useNativeMethod = method.isPrivate() ||
ReflectUtil.isPrivate(method.getDeclaringType());
boolean isThrowing = hasCheckedExceptions(method);
// Determine method signature parts.
String invokeeTypeName = ReflectUtil.getSourceName(method.getRawDeclaringType());
int invokerParamCount = method.getParameterTypes().size() + (hasInvokee ? 1 : 0);
TypeLiteral<?> returnType = method.getReturnType();
String returnTypeString = ReflectUtil.getSourceName(returnType);
boolean returning = !returnType.getRawType().equals(Void.TYPE);
String invokerMethodName = getInvokerMethodName(method, nameGenerator);
// The invoker method is placed in the fragment of the package that declares
// the method, so it has access to the same package-private types as the
// method declaration.
String invokerPackageName;
if (useNativeMethod && !hasInvokee) {
// In this case, the type of the invokee is not mentioned by the type
// signature of the invoker, and since we're using native code, we can
// write a call to the target method even if the invokee is fully private.
//
// This handles the case of a user statically injecting a private inner
// class.
//
// Pick a package somewhat arbitrarily; since we're using native code, it
// doesn't really matter where it goes. The declaring type's package is
// easy to get to:
invokerPackageName = method.getDeclaringType().getRawType().getPackage().getName();
} else {
invokerPackageName = ReflectUtil.getUserPackageName(method.getDeclaringType());
// TODO(dburrows): won't this silently fail if some *parameters* to the
// invokee have limited visibility? Currently I believe that we'll just
// generate noncompiling code.
}
methodsOutput.add(createInvoker(invokeeName, invokeeTypeName, hasInvokee, useNativeMethod,
isThrowing, invokerMethodName, invokerPackageName, invokerParamCount, method,
returnTypeString, returning, isLongAccess(method)));
return new InvokerCall(hasInvokee, invokeeName, invokerMethodName, invokerPackageName,
invokerParamCount, method, parameterNames);
}
/**
* Check whether a method needs to have Long access.
*/
private boolean isLongAccess(MethodLiteral<?, ?> method) {
boolean result = method.getReturnType().getRawType().equals(Long.TYPE);
for (TypeLiteral<?> paramLiteral : method.getParameterTypes()) {
result |= paramLiteral.getRawType().equals(Long.TYPE);
}
return result;
}
private static final class InvokerCall implements SourceSnippet {
private final boolean hasInvokee;
private final String invokeeName;
private final String invokerMethodName;
private final String invokerPackageName;
private final int invokerParamCount;
private final MethodLiteral<?, ?> method;
private final String[] parameterNames;
public InvokerCall(boolean hasInvokee, String invokeeName, String invokerMethodName,
String invokerPackageName, int invokerParamCount, MethodLiteral<?, ?> method,
String[] parameterNames) {
this.hasInvokee = hasInvokee;
this.invokeeName = invokeeName;
this.invokerMethodName = invokerMethodName;
this.invokerPackageName = invokerPackageName;
this.invokerParamCount = invokerParamCount;
this.method = method;
this.parameterNames = parameterNames;
}
@Override
public String getSource(InjectorWriteContext writeContext) {
// Collect method parameters to be passed to the actual method.
List<String> invokerCallParams = new ArrayList<String>(invokerParamCount);
if (hasInvokee) {
invokerCallParams.add(invokeeName);
}
int paramCount = 0;
for (Key<?> paramKey : method.getParameterKeys()) {
String paramName = ReflectUtil.formatParameterName(paramCount);
if (parameterNames[paramCount] != null) {
invokerCallParams.add(parameterNames[paramCount]);
} else {
invokerCallParams.add(writeContext.callGetter(paramKey));
}
paramCount++;
}
return writeContext.callMethod(invokerMethodName, invokerPackageName, invokerCallParams)
+ ";";
}
}
/**
* Create an invoker method. See {@link #createMethodCallWithInjection}.
*/
private InjectorMethod createInvoker(String invokeeName, String invokeeTypeName,
boolean hasInvokee, boolean isNative, boolean isThrowing, String invokerMethodName,
String invokerPackageName, int invokerParamCount, MethodLiteral<?, ?> method,
String returnTypeString, boolean returning, boolean isLongAccess)
throws NoSourceNameException {
List<String> invokerSignatureParams = new ArrayList<String>(invokerParamCount);
if (hasInvokee) {
invokerSignatureParams.add(invokeeTypeName + " invokee");
}
List<String> invokeeCallParams = new ArrayList<String>(method.getParameterTypes().size());
int paramCount = 0;
for (Key<?> paramKey : method.getParameterKeys()) {
String paramName = ReflectUtil.formatParameterName(paramCount);
// We cannot use the type literal of the key here: It is canonicalized
// during key creation, destroying some information, for example
// auto-boxing any primitives. This leads to type-mismatches when calling
// into JSNI. Instead we'll access the parameter's original type.
TypeLiteral<?> paramLiteral = method.getParameterTypes().get(paramCount);
invokerSignatureParams.add(ReflectUtil.getSourceName(paramLiteral) + " " + paramName);
invokeeCallParams.add(paramName);
paramCount++;
}
String annotation = isLongAccess ? "@com.google.gwt.core.client.UnsafeNativeLong " : "";
String invokerSignature = annotation + "public " + (isNative ? "native " : "")
+ returnTypeString + " " + invokerMethodName + "(" + join(", ", invokerSignatureParams)
+ ")";
return new InvokerMethod(hasInvokee, invokeeCallParams, invokeeTypeName, invokerPackageName,
invokerSignature, isNative, isThrowing, method, returning, returnTypeString);
}
private static final class InvokerMethod extends AbstractInjectorMethod {
private final boolean hasInvokee;
private final List<String> invokeeCallParams;
private final String invokeeTypeName;
private final boolean isThrowing;
private final MethodLiteral<?, ?> method;
private final boolean returning;
private final String returnTypeString;
public InvokerMethod(boolean hasInvokee, List<String> invokeeCallParams, String invokeeTypeName,
String invokerPackageName, String invokerSignature, boolean isNative, boolean isThrowing,
MethodLiteral<?, ?> method, boolean returning, String returnTypeString) {
super(isNative, invokerSignature, invokerPackageName);
this.hasInvokee = hasInvokee;
this.invokeeCallParams = invokeeCallParams;
this.invokeeTypeName = invokeeTypeName;
this.isThrowing = isThrowing;
this.method = method;
this.returning = returning;
this.returnTypeString = returnTypeString;
}
public String getMethodBody(InjectorWriteContext writeContext) throws NoSourceNameException {
StringBuilder result = new StringBuilder();
if (isThrowing) {
result.append("try {\n ");
}
if (returning) {
result.append("return ");
}
if (!isNative()) {
if (hasInvokee) {
result.append("invokee.").append(method.getName());
} else if (method.isConstructor()) {
result.append("new ").append(invokeeTypeName);
} else {
result.append(invokeeTypeName).append(".").append(method.getName());
}
} else {
if (hasInvokee) {
result.append("invokee.");
}
result.append(getJsniSignature(method));
}
result.append("(").append(join(", ", invokeeCallParams)).append(");");
if (isThrowing) {
if (!isNative()) {
result.append("\n} catch (Exception e) {\n")
.append(" throw new com.google.gwt.inject.client.CreationException(e);\n")
.append("}");
} else {
result.append("\n} catch (e) {\n")
.append(" throw @com.google.gwt.inject.client.CreationException")
.append("::new(Ljava/lang/Throwable;)(e);\n")
.append("}");
}
}
return result.toString();
}
}
/**
* Get the name of an invoker method. This has a side-effect: it returns a
* newly unique value each time it is called, so be sure not to call it twice
* for the same method call (you'll get two different names).
*/
private String getInvokerMethodName(MethodLiteral<?, ?> method,
NameGenerator nameGenerator) throws NoSourceNameException {
Class<?> methodDeclaringType = method.getRawDeclaringType();
String invokeeTypeName = ReflectUtil.getSourceName(methodDeclaringType);
String methodBaseName = nameGenerator.convertToValidMemberName(
String.format("%s_%s_methodInjection", invokeeTypeName, method.getName()));
return nameGenerator.createMethodName(methodBaseName);
}
private boolean hasCheckedExceptions(MethodLiteral<?, ?> method) {
return method.getExceptionTypes().size() > 0;
}
private static String getJsniSignature(MethodLiteral<?, ?> method) throws NoSourceNameException {
StringBuilder signature = new StringBuilder();
signature.append("@");
signature.append(ReflectUtil.getSourceName(method.getRawDeclaringType()));
String name = method.isConstructor() ? "new" : method.getName();
signature.append("::").append(name).append("(");
// Using raw parameter types here since JNI doesn't know about
// parametrization at lookup time.
for (Type param : method.getRawParameterTypes()) {
signature.append(getJniSignature(param));
}
signature.append(")");
return signature.toString();
}
private static String getJniSignature(Type type) throws NoSourceNameException {
if (type instanceof Class<?>) {
if (((Class) type).isPrimitive()) {
if (type.equals(Boolean.TYPE)) {
return "Z";
} else if (type.equals(Byte.TYPE)) {
return "B";
} else if (type.equals(Character.TYPE)) {
return "C";
} else if (type.equals(Double.TYPE)) {
return "D";
} else if (type.equals(Float.TYPE)) {
return "F";
} else if (type.equals(Integer.TYPE)) {
return "I";
} else if (type.equals(Long.TYPE)) {
return "J";
} else if (type.equals(Short.TYPE)) {
return "S";
}
}
return "L" + getBinaryName((Class) type) + ";";
}
if (type instanceof GenericArrayType) {
return "[" + getJniSignature(((GenericArrayType) type).getGenericComponentType());
}
if (type instanceof ParameterizedType) {
return getJniSignature(((ParameterizedType) type).getRawType());
}
if (type instanceof WildcardType) {
// TODO(schmitt): This is likely incorrect in some cases.
return getJniSignature(((WildcardType) type).getUpperBounds()[0]);
}
if (type instanceof TypeVariable) {
// TODO(schmitt): This is likely incorrect in some cases.
return getJniSignature(((TypeVariable) type).getBounds()[0]);
}
throw new NoSourceNameException(type);
}
private static String getBinaryName(Class<?> type) {
List<String> classes = new ArrayList<String>();
for (Class<?> clazz = type; clazz != null; clazz = clazz.getEnclosingClass()) {
classes.add(clazz.getSimpleName());
}
Collections.reverse(classes);
String packageName = type.getPackage().getName().replace('.', '/');
if (packageName.length() > 0) {
packageName += "/";
}
return packageName + join("$", classes);
}
}