blob: 4a352fc987bba576a66ff69cc69a51eb55e48ea3 [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.bsf.util;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Enumeration;
import java.util.Vector;
/**
* This file is a collection of reflection utilities for dealing with methods and constructors.
*/
public class MethodUtils {
/**
* Internal Class for getEntryPoint(). Implements 15.11.2.2 MORE SPECIFIC rules.
*
* Retains a list of methods (already known to match the arguments). As each method is added, we check against past entries to determine which if any is
* "more specific" -- defined as having _all_ its arguments (not just a preponderance) be method-convertable into those of another. If such a relationship
* is found, the more-specific method is retained and the less-specific method is discarded. At the end, if this has yielded a single winner it is
* considered the Most Specific Method and hence the one that should be invoked. Otherwise, a NoSuchMethodException is thrown.
*
* PERFORMANCE VERSUS ARCHITECTURE: Arguably, this should "have-a" Vector. But the code is 6% smaller, and possibly faster, if we code it as "is-a" Vector.
* Since it's an inner class, nobody's likely to abuse the privilage.
*
* Note: "Static" in the case of an inner class means "Does not reference instance data in the outer class", and is required since our caller is a static
* method.
*/
private static class MoreSpecific extends Vector {
/**
* Submit an entry-point to the list. May be discarded if a past entry is more specific, or may cause others to be discarded it if is more specific.
*
* newEntry: Method or Constructor under consideration.
*/
void addItem(final Object newEntry) {
if (size() == 0) {
addElement(newEntry);
} else {
final Class[] newargs = entryGetParameterTypes(newEntry);
boolean keep = true;
for (final Enumeration e = elements(); keep & e.hasMoreElements();) {
final Object oldEntry = e.nextElement();
// CAVEAT: Implicit references to enclosing class!
final Class[] oldargs = entryGetParameterTypes(oldEntry);
if (areMethodConvertable(oldargs, newargs)) {
removeElement(oldEntry); // New more specific; discard old
} else if (areMethodConvertable(newargs, oldargs)) {
keep = false; // Old more specific; discard new
// Else they're tied. Keep both and hope someone beats both.
}
}
if (keep) {
addElement(newEntry);
}
}
}
/**
* Obtain the single Most Specific entry-point. If there is no clear winner, or if the list is empty, throw NoSuchMethodException.
*
* Arguments describe the call we were hoping to resolve. They are used to throw a nice verbose exception if something goes wrong.
*/
Object getMostSpecific(final Class targetClass, final String methodName, final Class[] argTypes, final boolean isStaticReference)
throws NoSuchMethodException {
if (size() == 1) {
return firstElement();
}
if (size() > 1) {
final StringBuilder buf = new StringBuilder();
final Enumeration e = elements();
buf.append(e.nextElement());
while (e.hasMoreElements()) {
buf.append(" and ").append(e.nextElement());
}
throw new NoSuchMethodException(
callToString(targetClass, methodName, argTypes, isStaticReference) + " is ambiguous. It matches " + buf.toString());
}
return null;
}
}
/**
* Convenience method: Test an entire parameter-list/argument-list pair for isMethodConvertable(), qv.
*/
static private boolean areMethodConvertable(final Class[] parms, final Class[] args) {
if (parms.length != args.length) {
return false;
}
for (int i = 0; i < parms.length; ++i) {
if (!isMethodConvertable(parms[i], args[i])) {
return false;
}
}
return true;
}
/**
* Internal subroutine for getEntryPoint(): Format arguments as a string describing the function being searched for. Used in verbose exceptions.
*/
private static String callToString(final Class targetClass, final String methodName, final Class[] argTypes, final boolean isStaticReference) {
final StringBuilder buf = new StringBuilder();
if (isStaticReference) {
buf.append("static ");
}
buf.append(StringUtils.getClassName(targetClass));
if (methodName != null) {
buf.append(".").append(methodName);
}
buf.append("(");
if (argTypes != null && argTypes.length > 0) {
if (false) {
// ????? Sanjiva has an ArrayToString method. Using it would
// save a few bytes, at cost of giving up some reusability.
} else {
buf.append(StringUtils.getClassName(argTypes[0]));
for (int i = 1; i < argTypes.length; i++) {
buf.append(",").append(StringUtils.getClassName(argTypes[i]));
}
}
} else {
buf.append("[none]");
}
buf.append(")");
return buf.toString();
}
/**
* Utility function: obtain common data from either Method or Constructor. (In lieu of an EntryPoint interface.)
*/
static int entryGetModifiers(final Object entry) {
return (entry instanceof Method) ? ((Method) entry).getModifiers() : ((Constructor) entry).getModifiers();
}
// The common lookup code would be much easier if Method and
// Constructor shared an "EntryPoint" Interface. Unfortunately, even
// though their APIs are almost identical, they don't. These calls
// are a workaround... at the cost of additional runtime overhead
// and some extra bytecodes.
//
// (A JDK bug report has been submitted requesting that they add the
// Interface; it would be easy, harmless, and useful.)
/**
* Utility function: obtain common data from either Method or Constructor. (In lieu of an EntryPoint interface.)
*/
static String entryGetName(final Object entry) {
return (entry instanceof Method) ? ((Method) entry).getName() : ((Constructor) entry).getName();
}
/**
* Utility function: obtain common data from either Method or Constructor. (In lieu of an EntryPoint interface.)
*/
static Class[] entryGetParameterTypes(final Object entry) {
return (entry instanceof Method) ? ((Method) entry).getParameterTypes() : ((Constructor) entry).getParameterTypes();
}
/**
* Utility function: obtain common data from either Method or Constructor. (In lieu of an EntryPoint interface.)
*/
static String entryToString(final Object entry) {
return (entry instanceof Method) ? ((Method) entry).toString() : ((Constructor) entry).toString();
}
//////////////////////////////////////////////////////////////////////////
/**
* Class.getConstructor() finds only the entry point (if any) _exactly_ matching the specified argument types. Our implmentation can decide between several
* imperfect matches, using the same search algorithm as the Java compiler.
*
* Note that all constructors are static by definition, so isStaticReference is true.
*
* @exception NoSuchMethodException if constructor not found.
*/
static public Constructor getConstructor(final Class targetClass, final Class[] argTypes) throws SecurityException, NoSuchMethodException {
return (Constructor) getEntryPoint(targetClass, null, argTypes, true);
}
//////////////////////////////////////////////////////////////////////////
/**
* Search for entry point, per Java Language Spec 1.0 as amended, verified by comparison against compiler behavior.
*
* @param targetClass Class object for the class to be queried.
* @param methodName Name of method to invoke, or null for constructor. Only Public methods will be accepted.
* @param argTypes Classes of intended arguments. Note that primitives must be specified via their TYPE equivalents, rather than as their wrapper
* classes -- Integer.TYPE rather than Integer. "null" may be passed in as an indication that you intend to invoke the method with
* a literal null argument and therefore can accept any object type in this position.
* @param isStaticReference If true, and if the target is a Class object, only static methods will be accepted as valid matches.
*
* @return a Method or Constructor of the appropriate signature
*
* @exception SecurityException if security violation
* @exception NoSuchMethodException if no such method
*/
static private Object getEntryPoint(final Class targetClass, final String methodName, final Class[] argTypes, final boolean isStaticReference)
throws SecurityException, NoSuchMethodException {
// 15.11.1: OBTAIN STARTING CLASS FOR SEARCH
Object m = null;
// 15.11.2 DETERMINE ARGUMENT SIGNATURE
// (Passed in as argTypes array.)
// Shortcut: If an exact match exists, return it.
try {
if (methodName != null) {
m = targetClass.getMethod(methodName, argTypes);
if (isStaticReference && !Modifier.isStatic(entryGetModifiers(m))) {
throw new NoSuchMethodException(callToString(targetClass, methodName, argTypes, isStaticReference) + " resolved to instance " + m);
}
return m;
} else {
return targetClass.getConstructor(argTypes);
}
} catch (final NoSuchMethodException e) {
// no-args has no alternatives!
if (argTypes == null || argTypes.length == 0) {
throw new NoSuchMethodException(callToString(targetClass, methodName, argTypes, isStaticReference) + " not found.");
}
// Else fall through.
}
// Well, _that_ didn't work. Time to search for the Most Specific
// matching function. NOTE that conflicts are possible!
// 15.11.2.1 ACCESSIBLE: We apparently need to gather from two
// sources to be sure we have both instance and static methods.
Object[] methods;
if (methodName != null) {
methods = targetClass.getMethods();
} else {
methods = targetClass.getConstructors();
}
if (0 == methods.length) {
throw new NoSuchMethodException("No methods!");
}
final MoreSpecific best = new MoreSpecific();
for (int i = 0; i < methods.length; ++i) {
final Object mi = methods[i];
if (
// 15.11.2.1 ACCESSIBLE: Method is public.
Modifier.isPublic(entryGetModifiers(mi)) &&
// 15.11.2.1 APPLICABLE: Right method name (or c'tor)
(methodName == null || entryGetName(mi).equals(methodName)) &&
// 15.11.2.1 APPLICABLE: Parameters match arguments
areMethodConvertable(entryGetParameterTypes(mi), argTypes)) {
// 15.11.2.2 MORE SPECIFIC displace less specific.
best.addItem(mi);
}
}
// May throw NoSuchMethodException; we pass in info needed to
// create a useful exception
m = best.getMostSpecific(targetClass, methodName, argTypes, isStaticReference);
// 15.11.3 APPROPRIATE: Class invocation can call only static
// methods. Note that the defined order of evaluation permits a
// call to be resolved to an inappropriate method and then
// rejected, rather than finding the best of the appropriate
// methods.
//
// Constructors are never static, so we don't test them.
if (m == null) {
throw new NoSuchMethodException(callToString(targetClass, methodName, argTypes, isStaticReference) + " -- no signature match");
}
if (methodName != null && isStaticReference && !Modifier.isStatic(entryGetModifiers(m))) {
throw new NoSuchMethodException(callToString(targetClass, methodName, argTypes, isStaticReference) + " resolved to instance: " + m);
}
return m;
}
//////////////////////////////////////////////////////////////////////////
/*
* Class.getMethod() finds only the entry point (if any) _exactly_ matching the specified argument types. Our implmentation can decide between several
* imperfect matches, using the same search algorithm as the Java compiler.
*
* This version more closely resembles Class.getMethod() -- we always ask the Class for the method. It differs in testing for appropriateness before
* returning the method; if the query is being made via a static reference, only static methods will be found and returned.
*/
static public Method getMethod(final Class target, final String methodName, final Class[] argTypes, final boolean isStaticReference)
throws SecurityException, NoSuchMethodException {
return (Method) getEntryPoint(target, methodName, argTypes, isStaticReference);
}
//////////////////////////////////////////////////////////////////////////
/**
* Class.getMethod() finds only the entry point (if any) _exactly_ matching the specified argument types. Our implmentation can decide between several
* imperfect matches, using the same search algorithm as the Java compiler.
*
* This version emulates the compiler behavior by allowing lookup to be performed against either a class or an instance -- classname.foo() must be a static
* method call, instance.foo() can invoke either static or instance methods.
*
* @param target object on which call is to be made
* @param methodName name of method I'm lookin' for
* @param argTypes array of argument types of method
*
* @return the desired method
*
* @exception SecurityException if security violation
* @exception NoSuchMethodException if no such method
*/
static public Method getMethod(final Object target, final String methodName, final Class[] argTypes) throws SecurityException, NoSuchMethodException {
final boolean staticRef = target instanceof Class;
return getMethod(staticRef ? (Class) target : target.getClass(), methodName, argTypes, staticRef);
}
/**
* Determine whether a given type can accept assignments of another type. Note that class.isAssignable() is _not_ a complete test! (This method is not
* needed by getMethod() or getConstructor(), but is provided as a convenience for other users.)
*
* parm: The type given in the method's signature. arg: The type we want to pass in.
*
* Legal ASSIGNMENT CONVERSIONS (5.2) are METHOD CONVERSIONS (5.3) plus implicit narrowing of int to byte, short or char.
*/
static private boolean isAssignmentConvertable(final Class parm, final Class arg) {
return (arg.equals(Integer.TYPE) && (parm.equals(Byte.TYPE) || parm.equals(Short.TYPE) || parm.equals(Character.TYPE)))
|| isMethodConvertable(parm, arg);
}
/**
* Determine whether a given method parameter type can accept arguments of another type.
*
* parm: The type given in the method's signature. arg: The type we want to pass in.
*
* Legal METHOD CONVERSIONS (5.3) are Identity, Widening Primitive Conversion, or Widening Reference Conversion. NOTE that this is a subset of the legal
* ASSIGNMENT CONVERSIONS (5.2) -- in particular, we can't implicitly narrow int to byte, short or char.
*
* SPECIAL CASE: In order to permit invoking methods with literal "null" values, setting the arg Class to null will be taken as a request to match any Class
* type. POSSIBLE PROBLEM: This may match a primitive type, which really should not accept a null value... but I'm not sure how best to distinguish those,
* short of enumerating them
*/
static private boolean isMethodConvertable(Class parm, Class arg) {
if (parm.equals(arg)) {
return true;
}
// Accept any type EXCEPT primitives (which can't have null values).
if (arg == null) {
return !parm.isPrimitive();
}
// Arrays are convertable if their elements are convertable
// ????? Does this have to be done before isAssignableFrom, or
// does it successfully handle arrays of primatives?
while (parm.isArray()) {
if (!arg.isArray()) {
return false; // Unequal array depth
} else {
parm = parm.getComponentType();
arg = arg.getComponentType();
}
}
if (arg.isArray()) {
return false; // Unequal array depth
}
// Despite its name, the 1.1.6 docs say that this function does
// NOT return true for all legal ASSIGNMENT CONVERSIONS
// (5.2):
// "Specifically, this method tests whether the type
// represented by the specified class can be converted
// to the type represented by this Class object via
// an identity conversion or via a widening reference
// conversion."
if (parm.isAssignableFrom(arg)) {
return true;
}
// That leaves us the Widening Primitives case. Four possibilities:
// void (can only convert to void), boolean (can only convert to boolean),
// numeric (which are sequenced) and char (which inserts itself into the
// numerics by promoting to int or larger)
if (parm.equals(Void.TYPE) || parm.equals(Boolean.TYPE) || arg.equals(Void.TYPE) || arg.equals(Boolean.TYPE)) {
return false;
}
final Class[] primTypes = { Character.TYPE, Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE };
int parmscore, argscore;
for (parmscore = 0; parmscore < primTypes.length; ++parmscore) {
if (parm.equals(primTypes[parmscore])) {
break;
}
}
if (parmscore >= primTypes.length) {
return false; // Off the end
}
for (argscore = 0; argscore < primTypes.length; ++argscore) {
if (arg.equals(primTypes[argscore])) {
break;
}
}
if (argscore >= primTypes.length) {
return false; // Off the end
}
// OK if ordered AND NOT char-to-smaller-than-int
return (argscore < parmscore && (argscore != 0 || parmscore > 2));
}
}