blob: 3609b7545acfdb7c668831dc68d30bdab9e3fb3a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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 org.apache.calcite.util;
import org.apache.calcite.linq4j.function.Parameter;
import org.apache.calcite.linq4j.tree.Primitive;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Static utilities for Java reflection.
*/
public abstract class ReflectUtil {
//~ Static fields/initializers ---------------------------------------------
private static Map<Class, Class> primitiveToBoxingMap;
private static Map<Class, Method> primitiveToByteBufferReadMethod;
private static Map<Class, Method> primitiveToByteBufferWriteMethod;
static {
primitiveToBoxingMap = new HashMap<>();
primitiveToBoxingMap.put(Boolean.TYPE, Boolean.class);
primitiveToBoxingMap.put(Byte.TYPE, Byte.class);
primitiveToBoxingMap.put(Character.TYPE, Character.class);
primitiveToBoxingMap.put(Double.TYPE, Double.class);
primitiveToBoxingMap.put(Float.TYPE, Float.class);
primitiveToBoxingMap.put(Integer.TYPE, Integer.class);
primitiveToBoxingMap.put(Long.TYPE, Long.class);
primitiveToBoxingMap.put(Short.TYPE, Short.class);
primitiveToByteBufferReadMethod = new HashMap<>();
primitiveToByteBufferWriteMethod = new HashMap<>();
Method[] methods = ByteBuffer.class.getDeclaredMethods();
for (Method method : methods) {
Class[] paramTypes = method.getParameterTypes();
if (method.getName().startsWith("get")) {
if (!method.getReturnType().isPrimitive()) {
continue;
}
if (paramTypes.length != 1) {
continue;
}
primitiveToByteBufferReadMethod.put(
method.getReturnType(), method);
// special case for Boolean: treat as byte
if (method.getReturnType().equals(Byte.TYPE)) {
primitiveToByteBufferReadMethod.put(Boolean.TYPE, method);
}
} else if (method.getName().startsWith("put")) {
if (paramTypes.length != 2) {
continue;
}
if (!paramTypes[1].isPrimitive()) {
continue;
}
primitiveToByteBufferWriteMethod.put(paramTypes[1], method);
// special case for Boolean: treat as byte
if (paramTypes[1].equals(Byte.TYPE)) {
primitiveToByteBufferWriteMethod.put(Boolean.TYPE, method);
}
}
}
}
//~ Methods ----------------------------------------------------------------
/**
* Uses reflection to find the correct java.nio.ByteBuffer "absolute get"
* method for a given primitive type.
*
* @param clazz the Class object representing the primitive type
* @return corresponding method
*/
public static Method getByteBufferReadMethod(Class clazz) {
assert clazz.isPrimitive();
return primitiveToByteBufferReadMethod.get(clazz);
}
/**
* Uses reflection to find the correct java.nio.ByteBuffer "absolute put"
* method for a given primitive type.
*
* @param clazz the Class object representing the primitive type
* @return corresponding method
*/
public static Method getByteBufferWriteMethod(Class clazz) {
assert clazz.isPrimitive();
return primitiveToByteBufferWriteMethod.get(clazz);
}
/**
* Gets the Java boxing class for a primitive class.
*
* @param primitiveClass representative class for primitive (e.g.
* java.lang.Integer.TYPE)
* @return corresponding boxing Class (e.g. java.lang.Integer)
*/
public static Class getBoxingClass(Class primitiveClass) {
assert primitiveClass.isPrimitive();
return primitiveToBoxingMap.get(primitiveClass);
}
/**
* Gets the name of a class with no package qualifiers; if it's an inner
* class, it will still be qualified by the containing class (X$Y).
*
* @param c the class of interest
* @return the unqualified name
*/
public static String getUnqualifiedClassName(Class c) {
String className = c.getName();
int lastDot = className.lastIndexOf('.');
if (lastDot < 0) {
return className;
}
return className.substring(lastDot + 1);
}
/**
* Composes a string representing a human-readable method name (with neither
* exception nor return type information).
*
* @param declaringClass class on which method is defined
* @param methodName simple name of method without signature
* @param paramTypes method parameter types
* @return unmangled method name
*/
public static String getUnmangledMethodName(
Class declaringClass,
String methodName,
Class[] paramTypes) {
StringBuilder sb = new StringBuilder();
sb.append(declaringClass.getName());
sb.append(".");
sb.append(methodName);
sb.append("(");
for (int i = 0; i < paramTypes.length; ++i) {
if (i > 0) {
sb.append(", ");
}
sb.append(paramTypes[i].getName());
}
sb.append(")");
return sb.toString();
}
/**
* Composes a string representing a human-readable method name (with neither
* exception nor return type information).
*
* @param method method whose name is to be generated
* @return unmangled method name
*/
public static String getUnmangledMethodName(
Method method) {
return getUnmangledMethodName(
method.getDeclaringClass(),
method.getName(),
method.getParameterTypes());
}
/**
* Implements the {@link org.apache.calcite.util.Glossary#VISITOR_PATTERN} via
* reflection. The basic technique is taken from <a
* href="http://www.javaworld.com/javaworld/javatips/jw-javatip98.html">a
* Javaworld article</a>. For an example of how to use it, see
* {@code ReflectVisitorTest}.
*
* <p>Visit method lookup follows the same rules as if
* compile-time resolution for VisitorClass.visit(VisiteeClass) were
* performed. An ambiguous match due to multiple interface inheritance
* results in an IllegalArgumentException. A non-match is indicated by
* returning false.
*
* @param visitor object whose visit method is to be invoked
* @param visitee object to be passed as a parameter to the visit
* method
* @param hierarchyRoot if non-null, visitor method will only be invoked if
* it takes a parameter whose type is a subtype of
* hierarchyRoot
* @param visitMethodName name of visit method, e.g. "visit"
* @return true if a matching visit method was found and invoked
*/
public static boolean invokeVisitor(
ReflectiveVisitor visitor,
Object visitee,
Class hierarchyRoot,
String visitMethodName) {
return invokeVisitorInternal(
visitor,
visitee,
hierarchyRoot,
visitMethodName);
}
/**
* Shared implementation of the two forms of invokeVisitor.
*
* @param visitor object whose visit method is to be invoked
* @param visitee object to be passed as a parameter to the visit
* method
* @param hierarchyRoot if non-null, visitor method will only be invoked if
* it takes a parameter whose type is a subtype of
* hierarchyRoot
* @param visitMethodName name of visit method, e.g. "visit"
* @return true if a matching visit method was found and invoked
*/
private static boolean invokeVisitorInternal(
Object visitor,
Object visitee,
Class hierarchyRoot,
String visitMethodName) {
Class<?> visitorClass = visitor.getClass();
Class visiteeClass = visitee.getClass();
Method method =
lookupVisitMethod(
visitorClass,
visiteeClass,
visitMethodName);
if (method == null) {
return false;
}
if (hierarchyRoot != null) {
Class paramType = method.getParameterTypes()[0];
if (!hierarchyRoot.isAssignableFrom(paramType)) {
return false;
}
}
try {
method.invoke(
visitor,
visitee);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch (InvocationTargetException ex) {
// visit methods aren't allowed to have throws clauses,
// so the only exceptions which should come
// to us are RuntimeExceptions and Errors
throw Util.throwAsRuntime(Util.causeOrSelf(ex));
}
return true;
}
/**
* Looks up a visit method.
*
* @param visitorClass class of object whose visit method is to be invoked
* @param visiteeClass class of object to be passed as a parameter to the
* visit method
* @param visitMethodName name of visit method
* @return method found, or null if none found
*/
public static Method lookupVisitMethod(
Class<?> visitorClass,
Class<?> visiteeClass,
String visitMethodName) {
return lookupVisitMethod(
visitorClass,
visiteeClass,
visitMethodName,
Collections.emptyList());
}
/**
* Looks up a visit method taking additional parameters beyond the
* overloaded visitee type.
*
* @param visitorClass class of object whose visit method is to be
* invoked
* @param visiteeClass class of object to be passed as a parameter
* to the visit method
* @param visitMethodName name of visit method
* @param additionalParameterTypes list of additional parameter types
* @return method found, or null if none found
* @see #createDispatcher(Class, Class)
*/
public static Method lookupVisitMethod(
Class<?> visitorClass,
Class<?> visiteeClass,
String visitMethodName,
List<Class> additionalParameterTypes) {
// Prepare an array to re-use in recursive calls. The first argument
// will have the visitee class substituted into it.
Class<?>[] paramTypes = new Class[1 + additionalParameterTypes.size()];
int iParam = 0;
paramTypes[iParam++] = null;
for (Class<?> paramType : additionalParameterTypes) {
paramTypes[iParam++] = paramType;
}
// Cache Class to candidate Methods, to optimize the case where
// the original visiteeClass has a diamond-shaped interface inheritance
// graph. (This is common, for example, in JMI.) The idea is to avoid
// iterating over a single interface's method more than once in a call.
Map<Class<?>, Method> cache = new HashMap<>();
return lookupVisitMethod(
visitorClass,
visiteeClass,
visitMethodName,
paramTypes,
cache);
}
private static Method lookupVisitMethod(
final Class<?> visitorClass,
final Class<?> visiteeClass,
final String visitMethodName,
final Class<?>[] paramTypes,
final Map<Class<?>, Method> cache) {
// Use containsKey since the result for a Class might be null.
if (cache.containsKey(visiteeClass)) {
return cache.get(visiteeClass);
}
Method candidateMethod = null;
paramTypes[0] = visiteeClass;
try {
candidateMethod =
visitorClass.getMethod(
visitMethodName,
paramTypes);
cache.put(visiteeClass, candidateMethod);
return candidateMethod;
} catch (NoSuchMethodException ex) {
// not found: carry on with lookup
}
Class<?> superClass = visiteeClass.getSuperclass();
if (superClass != null) {
candidateMethod =
lookupVisitMethod(
visitorClass,
superClass,
visitMethodName,
paramTypes,
cache);
}
Class<?>[] interfaces = visiteeClass.getInterfaces();
for (Class<?> anInterface : interfaces) {
final Method method = lookupVisitMethod(visitorClass, anInterface,
visitMethodName, paramTypes, cache);
if (method != null) {
if (candidateMethod != null) {
if (!method.equals(candidateMethod)) {
Class<?> c1 = method.getParameterTypes()[0];
Class<?> c2 = candidateMethod.getParameterTypes()[0];
if (c1.isAssignableFrom(c2)) {
// c2 inherits from c1, so keep candidateMethod
// (which is more specific than method)
continue;
} else if (c2.isAssignableFrom(c1)) {
// c1 inherits from c2 (method is more specific
// than candidate method), so fall through
// to set candidateMethod = method
} else {
// c1 and c2 are not directly related
throw new IllegalArgumentException("dispatch ambiguity between "
+ candidateMethod + " and " + method);
}
}
}
candidateMethod = method;
}
}
cache.put(visiteeClass, candidateMethod);
return candidateMethod;
}
/**
* Creates a dispatcher for calls to {@link #lookupVisitMethod}. The
* dispatcher caches methods between invocations.
*
* @param visitorBaseClazz Visitor base class
* @param visiteeBaseClazz Visitee base class
* @return cache of methods
*/
public static <R extends ReflectiveVisitor, E> ReflectiveVisitDispatcher<R, E> createDispatcher(
final Class<R> visitorBaseClazz,
final Class<E> visiteeBaseClazz) {
assert ReflectiveVisitor.class.isAssignableFrom(visitorBaseClazz);
assert Object.class.isAssignableFrom(visiteeBaseClazz);
return new ReflectiveVisitDispatcher<R, E>() {
final Map<List<Object>, Method> map = new HashMap<>();
public Method lookupVisitMethod(
Class<? extends R> visitorClass,
Class<? extends E> visiteeClass,
String visitMethodName) {
return lookupVisitMethod(
visitorClass,
visiteeClass,
visitMethodName,
Collections.emptyList());
}
public Method lookupVisitMethod(
Class<? extends R> visitorClass,
Class<? extends E> visiteeClass,
String visitMethodName,
List<Class> additionalParameterTypes) {
final List<Object> key =
ImmutableList.of(
visitorClass,
visiteeClass,
visitMethodName,
additionalParameterTypes);
Method method = map.get(key);
if (method == null) {
if (map.containsKey(key)) {
// We already looked for the method and found nothing.
} else {
method =
ReflectUtil.lookupVisitMethod(
visitorClass,
visiteeClass,
visitMethodName,
additionalParameterTypes);
map.put(key, method);
}
}
return method;
}
public boolean invokeVisitor(
R visitor,
E visitee,
String visitMethodName) {
return ReflectUtil.invokeVisitor(
visitor,
visitee,
visiteeBaseClazz,
visitMethodName);
}
};
}
/**
* Creates a dispatcher for calls to a single multi-method on a particular
* object.
*
* <p>Calls to that multi-method are resolved by looking for a method on
* the runtime type of that object, with the required name, and with
* the correct type or a subclass for the first argument, and precisely the
* same types for other arguments.
*
* <p>For instance, a dispatcher created for the method
*
* <blockquote>String foo(Vehicle, int, List)</blockquote>
*
* <p>could be used to call the methods
*
* <blockquote>String foo(Car, int, List)<br>
* String foo(Bus, int, List)</blockquote>
*
* <p>(because Car and Bus are subclasses of Vehicle, and they occur in the
* polymorphic first argument) but not the method
*
* <blockquote>String foo(Car, int, ArrayList)</blockquote>
*
* <p>(only the first argument is polymorphic).
*
* <p>You must create an implementation of the method for the base class.
* Otherwise throws {@link IllegalArgumentException}.
*
* @param returnClazz Return type of method
* @param visitor Object on which to invoke the method
* @param methodName Name of method
* @param arg0Clazz Base type of argument zero
* @param otherArgClasses Types of remaining arguments
*/
public static <E, T> MethodDispatcher<T> createMethodDispatcher(
final Class<T> returnClazz,
final ReflectiveVisitor visitor,
final String methodName,
final Class<E> arg0Clazz,
final Class... otherArgClasses) {
final List<Class> otherArgClassList =
ImmutableList.copyOf(otherArgClasses);
@SuppressWarnings({"unchecked" })
final ReflectiveVisitDispatcher<ReflectiveVisitor, E>
dispatcher =
createDispatcher(
(Class<ReflectiveVisitor>) visitor.getClass(), arg0Clazz);
return new MethodDispatcher<T>() {
public T invoke(Object... args) {
Method method = lookupMethod(args[0]);
try {
final Object o = method.invoke(visitor, args);
return returnClazz.cast(o);
} catch (IllegalAccessException e) {
throw new RuntimeException("While invoking method '" + method + "'",
e);
} catch (InvocationTargetException e) {
final Throwable target = e.getTargetException();
if (target instanceof RuntimeException) {
throw (RuntimeException) target;
}
if (target instanceof Error) {
throw (Error) target;
}
throw new RuntimeException("While invoking method '" + method + "'",
e);
}
}
private Method lookupMethod(final Object arg0) {
if (!arg0Clazz.isInstance(arg0)) {
throw new IllegalArgumentException();
}
Method method =
dispatcher.lookupVisitMethod(
visitor.getClass(),
(Class<? extends E>) arg0.getClass(),
methodName,
otherArgClassList);
if (method == null) {
List<Class> classList = new ArrayList<>();
classList.add(arg0Clazz);
classList.addAll(otherArgClassList);
throw new IllegalArgumentException("Method not found: " + methodName
+ "(" + classList + ")");
}
return method;
}
};
}
/** Derives the name of the {@code i}th parameter of a method. */
public static String getParameterName(Method method, int i) {
for (Annotation annotation : method.getParameterAnnotations()[i]) {
if (annotation.annotationType() == Parameter.class) {
return ((Parameter) annotation).name();
}
}
return method.getParameters()[i].getName();
}
/** Derives whether the {@code i}th parameter of a method is optional. */
public static boolean isParameterOptional(Method method, int i) {
for (Annotation annotation : method.getParameterAnnotations()[i]) {
if (annotation.annotationType() == Parameter.class) {
return ((Parameter) annotation).optional();
}
}
return false;
}
/** Returns whether a parameter of a given type could possibly have an
* argument of a given type.
*
* <p>For example, consider method
*
* <blockquote>
* {@code foo(Object o, String s, int i, Number n, BigDecimal d}
* </blockquote>
*
* <p>To which which of those parameters could I pass a value that is an
* instance of {@link java.util.HashMap}? The answer:
*
* <ul>
* <li>{@code o} yes,
* <li>{@code s} no ({@code String} is a final class),
* <li>{@code i} no,
* <li>{@code n} yes ({@code Number} is an interface, and {@code HashMap} is
* a non-final class, so I could create a sub-class of {@code HashMap}
* that implements {@code Number},
* <li>{@code d} yes ({@code BigDecimal} is a non-final class).
* </ul>
*/
public static boolean mightBeAssignableFrom(Class<?> parameterType,
Class<?> argumentType) {
// TODO: think about arrays (e.g. int[] and String[])
if (parameterType == argumentType) {
return true;
}
if (Primitive.is(argumentType)) {
return false;
}
if (!parameterType.isInterface()
&& Modifier.isFinal(parameterType.getModifiers())) {
// parameter is a final class
// e.g. parameter String, argument Serializable
// e.g. parameter String, argument Map
// e.g. parameter String, argument Object
// e.g. parameter String, argument HashMap
return argumentType.isAssignableFrom(parameterType);
} else {
// parameter is an interface or non-final class
if (!argumentType.isInterface()
&& Modifier.isFinal(argumentType.getModifiers())) {
// argument is a final class
// e.g. parameter Object, argument String
// e.g. parameter Serializable, argument String
return parameterType.isAssignableFrom(argumentType);
} else {
// argument is an interface or non-final class
// e.g. parameter Map, argument Number
return true;
}
}
}
//~ Inner Classes ----------------------------------------------------------
/**
* Can invoke a method on an object of type E with return type T.
*
* @param <T> Return type of method
*/
public interface MethodDispatcher<T> {
/**
* Invokes method on an object with a given set of arguments.
*
* @param args Arguments to method
* @return Return value of method
*/
T invoke(Object... args);
}
}