diff --git a/projects/samskivert/src/java/com/samskivert/util/ClassUtil.java b/projects/samskivert/src/java/com/samskivert/util/ClassUtil.java new file mode 100644 index 00000000..885880b9 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/util/ClassUtil.java @@ -0,0 +1,300 @@ +// +// $Id: ClassUtil.java,v 1.1 2001/10/03 02:09:12 mdb Exp $ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001 Michael Bayne +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.util; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +/** + * Class object related utility routines. + * + *

This code was adapted from code provided by Paul Hosler in an + * article for Java Report Online + * (http://www.javareport.com/html/from_pages/article.asp?id=4276). His + * code was adapted from: + * + * + * Kaplan, Lowell. "More programming with generic interfaces." Java + * Report, Vol. 5, No. 2, Feb. 2000, pp. 70-74. + * + */ +public class ClassUtil +{ + /** + * @param clazz a class. + * + * @return true if the class is accessible, false otherwise. + * Presently returns true if the class is declared public. + */ + public static boolean classIsAccessible (Class clazz) + { + return Modifier.isPublic(clazz.getModifiers()); + } + + /** + * @param args an object array. + * + * @return an array of Class objects representing the classes of the + * objects in the given Object array. If args is null, a zero-length + * Class array is returned. If an element in args is null, then + * {@link Void#TYPE} is the corresponding Class in the return array. + */ + public static Class[] getParameterTypesFrom (Object[] args) + { + Class[] argTypes = null; + + if (args != null) { + argTypes = new Class[args.length]; + + for (int i = 0; i < args.length; ++i) { + argTypes[i] = (args[i] == null) ? + Void.TYPE : args[i].getClass(); + } + + } else { + argTypes = new Class[0]; + } + + return argTypes; + } + + /** + * Tells whether instances of the classes in the 'rhs' array could be + * used as parameters to a reflective method invocation whose + * parameter list has types denoted by the 'lhs' array. + * + * @param lhs Class array representing the types of the formal + * parameters of a method. + * @param rhs Class array representing the types of the actual + * parameters of a method. A null value or Void.TYPE is considered to + * match a corresponding Object or array class in lhs, but not a + * primitive. + * + * @return true if compatible, false otherwise. + */ + public static boolean compatibleClasses (Class[] lhs, Class[] rhs) + { + if (lhs.length != rhs.length) { + return false; + } + + for (int i = 0; i < lhs.length; ++i) { + if (rhs[i] == null || rhs[i].equals(Void.TYPE)) { + if (lhs[i].isPrimitive()) + return false; + else + continue; + } + + if (! lhs[i].isAssignableFrom(rhs[i])) { + Class lhsPrimEquiv = primitiveEquivalentOf(lhs[i]); + Class rhsPrimEquiv = primitiveEquivalentOf(rhs[i]); + + if (! primitiveIsAssignableFrom(lhsPrimEquiv, rhsPrimEquiv)) + return false; + } + } + + return true; + } + + /** + * Searches for the method with the given name and formal parameter + * types that is in the nearest accessible class in the class + * hierarchy, starting with clazz's superclass. The superclass and + * implemented interfaces of clazz are searched, then their + * superclasses, etc. until a method is found. Returns null if there + * is no such method. + * + * @param clazz a class. + * @param methodName name of a method. + * @param paramTypes Class array representing the types of a method's + * formal parameters. + * + * @return the nearest method located, or null if there is no such + * method. + */ + public static Method getAccessibleMethodFrom ( + Class clazz, String methodName, Class[] parameterTypes) + { + // Look for overridden method in the superclass. + Class superclass = clazz.getSuperclass(); + Method overriddenMethod = null; + + if (superclass != null && classIsAccessible(superclass)) { + try { + overriddenMethod = + superclass.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException _) { + } + + if (overriddenMethod != null) { + return overriddenMethod; + } + } + + // If here, then clazz represents Object, or an interface, or the + // superclass did not have an override. Check implemented + // interfaces. + Class[] interfaces = clazz.getInterfaces(); + + for (int i = 0; i < interfaces.length; ++i) { + overriddenMethod = null; + + if (classIsAccessible(interfaces[i])) { + try { + overriddenMethod = + interfaces[i].getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException _) { + } + + if (overriddenMethod != null) { + return overriddenMethod; + } + } + } + + overriddenMethod = null; + + // Try superclass's superclass and implemented interfaces. + if (superclass != null) { + overriddenMethod = getAccessibleMethodFrom( + superclass, methodName, parameterTypes); + + if (overriddenMethod != null) { + return overriddenMethod; + } + } + + // Try implemented interfaces' extended interfaces... + for (int i = 0; i < interfaces.length; ++i) { + overriddenMethod = getAccessibleMethodFrom( + interfaces[i], methodName, parameterTypes); + + if (overriddenMethod != null) { + return overriddenMethod; + } + } + + // Give up. + return null; + } + + /** + * @param clazz a Class. + * + * @return the class's primitive equivalent, if clazz is a primitive + * wrapper. If clazz is primitive, returns clazz. Otherwise, + * returns null. + */ + public static Class primitiveEquivalentOf (Class clazz) + { + return clazz.isPrimitive() ? + clazz : (Class)_objectToPrimitiveMap.get(clazz); + } + + /** + * Tells whether an instance of the primitive class represented by + * 'rhs' can be assigned to an instance of the primitive class + * represented by 'lhs'. + * + * @param lhs assignee class. + * @param rhs assigned class. + * + * @return true if compatible, false otherwise. If either argument is + * null, or one of the parameters does not represent a + * primitive (e.g. Byte.TYPE), returns false. + */ + public static boolean primitiveIsAssignableFrom (Class lhs, Class rhs) + { + if (lhs == null || rhs == null) + return false; + + if (! (lhs.isPrimitive() && rhs.isPrimitive())) + return false; + + if (lhs.equals(rhs)) + return true; + + Set wideningSet = (Set)_primitiveWideningsMap.get(rhs); + if (wideningSet == null) + return false; + + return wideningSet.contains(lhs); + } + + /** + * Mapping from primitive wrapper Classes to their corresponding + * primitive Classes. + */ + private static final Map _objectToPrimitiveMap = new HashMap(13); + + static { + _objectToPrimitiveMap.put(Boolean.class, Boolean.TYPE); + _objectToPrimitiveMap.put(Byte.class, Byte.TYPE); + _objectToPrimitiveMap.put(Character.class, Character.TYPE); + _objectToPrimitiveMap.put(Double.class, Double.TYPE); + _objectToPrimitiveMap.put(Float.class, Float.TYPE); + _objectToPrimitiveMap.put(Integer.class, Integer.TYPE); + _objectToPrimitiveMap.put(Long.class, Long.TYPE); + _objectToPrimitiveMap.put(Short.class, Short.TYPE); + } + + /** + * Mapping from primitive wrapper Classes to the sets of primitive + * classes whose instances can be assigned an instance of the first. + */ + private static final Map _primitiveWideningsMap = new HashMap(11); + + static { + Set set = new HashSet(); + set.add(Short.TYPE); + set.add(Integer.TYPE); + set.add(Long.TYPE); + set.add(Float.TYPE); + set.add(Double.TYPE); + _primitiveWideningsMap.put(Byte.TYPE, set); + + set = new HashSet(); + set.add(Integer.TYPE); + set.add(Long.TYPE); + set.add(Float.TYPE); + set.add(Double.TYPE); + _primitiveWideningsMap.put(Short.TYPE, set); + _primitiveWideningsMap.put(Character.TYPE, set); + + set = new HashSet(); + set.add(Long.TYPE); + set.add(Float.TYPE); + set.add(Double.TYPE); + _primitiveWideningsMap.put(Integer.TYPE, set); + + set = new HashSet(); + set.add(Float.TYPE); + set.add(Double.TYPE); + _primitiveWideningsMap.put(Long.TYPE, set); + + set = new HashSet(); + set.add(Double.TYPE); + _primitiveWideningsMap.put(Float.TYPE, set); + } +} diff --git a/projects/samskivert/src/java/com/samskivert/util/MethodFinder.java b/projects/samskivert/src/java/com/samskivert/util/MethodFinder.java new file mode 100644 index 00000000..b7fa7e4a --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/util/MethodFinder.java @@ -0,0 +1,365 @@ +// +// $Id: MethodFinder.java,v 1.1 2001/10/03 02:09:12 mdb Exp $ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001 Michael Bayne +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.util; + +import java.lang.reflect.*; +import java.util.*; + +/** + * Finds methods and constructors that can be invoked reflectively. + * Attempts to address some of the limitations of the JDK's {@link + * Class#getMethod} and {@link Class#getConstructor}, and other JDK + * reflective facilities. + * + *

Because those methods only match exact method signatures, one is + * unable to perform the same method matching that the compiler does at + * compile time (e.g. matching the method foo(Exception) when + * the user wants to call a method named foo with an + * IOException argument) with the basic reflection services. + * This class implements the method resolution process according to the + * same rules used by a Java compiler. These rules are outlined in the + * Java Language Specification, variously in sections 5.1.2, 5.1.4, 5.3, + * and 15.12.2. + * + *

This code was adapted from code provided by Paul Hosler in an + * article for Java Report Online + * (http://www.javareport.com/html/from_pages/article.asp?id=4276). His + * code was adapted from: + * + * + * Kaplan, Lowell. "More programming with generic interfaces." Java + * Report, Vol. 5, No. 2, Feb. 2000, pp. 70-74. + * + */ +public final class MethodFinder +{ + /** + * Constructs a method finder for the supplied class. + * + * @param clazz Class in which I will look for methods and + * constructors. + * + * @exception IllegalArgumentException if clazz is null, or represents + * a primitive, or represents an array type. + */ + public MethodFinder (Class clazz) + { + if (clazz == null) { + throw new IllegalArgumentException("null Class parameter"); + } + + if (clazz.isPrimitive()) { + throw new IllegalArgumentException( + "primitive Class parameter"); + } + + if (clazz.isArray()) { + throw new IllegalArgumentException( + "array Class parameter"); + } + + this.clazz = clazz; + } + + public boolean equals (Object o) + { + if (this == o) + return true; + else if (o == null || getClass() != o.getClass()) + return false; + else { + MethodFinder other = (MethodFinder) o; + return clazz.equals(other.clazz); + } + } + + /** + * Returns the most specific public constructor in my target class + * that accepts the number and type of parameters in the given Class + * array in a reflective invocation. + * + *

A null value or {@link Void#TYPE} in parameterTypes matches a + * corresponding Object or array reference in a constructor's formal + * parameter list, but not a primitive formal parameter. + * + * @param parameterTypes array representing the number and types of + * parameters to look for in the constructor's signature. A null + * array is treated as a zero-length array. + * + * @return Constructor object satisfying the conditions. + * + * @exception NoSuchMethodException if no constructors match the + * criteria, or if the reflective call is ambiguous based on the + * parameter types. + */ + public Constructor findConstructor (Class[] parameterTypes) + throws NoSuchMethodException + { + // make sure the constructor list is loaded + maybeLoadConstructors(); + + if (parameterTypes == null) { + parameterTypes = new Class[0]; + } + + return (Constructor) findMemberIn(ctorList, parameterTypes); + } + + /** + * Returns the most specific public method in my target class that has + * the given name and accepts the number and type of parameters in the + * given Class array in a reflective invocation. + * + *

A null value or {@link Void#TYPE} in parameterTypes will match + * a corresponding Object or array reference in a method's formal + * parameter list, but not a primitive formal parameter. + * + * @param methodName name of the method to search for. + * @param parameterTypes array representing the number and types of + * parameters to look for in the method's signature. A null array is + * treated as a zero-length array. + * + * @return Method object satisfying the conditions. + * + * @exception NoSuchMethodException if no methods match the criteria, + * or if the reflective call is ambiguous based on the parameter + * types, or if methodName is null. + */ + public Method findMethod (String methodName, Class[] parameterTypes) + throws NoSuchMethodException + { + // make sure the constructor list is loaded + maybeLoadMethods(); + + List methodList = (List) methodMap.get(methodName); + if (methodList == null) { + throw new NoSuchMethodException( + "No method named " + clazz.getName() + "." + methodName); + } + + if (parameterTypes == null) { + parameterTypes = new Class[0]; + } + + return (Method) findMemberIn(methodList, parameterTypes); + } + + /** + * Basis of {@link #findConstructor} and {@link #findMethod}. The + * member list fed to this method will be either all {@link + * Constructor} objects or all {@link Method} objects. + */ + private Member findMemberIn (List memberList, Class[] parameterTypes) + throws NoSuchMethodException + { + List matchingMembers = new ArrayList(); + + for (Iterator it = memberList.iterator(); it.hasNext();) { + Member member = (Member) it.next(); + Class[] methodParamTypes = (Class[]) paramMap.get(member); + + // check for exactly equal method signature + if (Arrays.equals(methodParamTypes, parameterTypes)) { + return member; + } + + if (ClassUtil.compatibleClasses( + methodParamTypes, parameterTypes)) { + matchingMembers.add(member); + } + } + + if (matchingMembers.isEmpty()) { + throw new NoSuchMethodException( + "No member in " + clazz.getName() + " matching given args"); + } + if (matchingMembers.size() == 1) { + return (Member) matchingMembers.get(0); + } + + return findMostSpecificMemberIn(matchingMembers); + } + + /** + * @param memberList a list of members (either all constructors or all + * methods). + * + * @return the most specific of all members in the list. + * + * @exception NoSuchMethodException if there is an ambiguity as to + * which is most specific. + */ + private Member findMostSpecificMemberIn (List memberList) + throws NoSuchMethodException + { + List mostSpecificMembers = new ArrayList(); + + for (Iterator memberIt = memberList.iterator(); + memberIt.hasNext();) { + Member member = (Member) memberIt.next(); + + if (mostSpecificMembers.isEmpty()) { + // First guy in is the most specific so far. + mostSpecificMembers.add(member); + + } else { + boolean moreSpecific = true; + boolean lessSpecific = false; + + // Is member more specific than everyone in the + // most-specific set? + for (Iterator specificIt = mostSpecificMembers.iterator(); + specificIt.hasNext();) { + Member moreSpecificMember = (Member) specificIt.next(); + + if (! memberIsMoreSpecific(member, moreSpecificMember)) { + // if the candidate member is not more specific + // than this member, then it's not more specific + // than the entire set, but it may still be + // equivalently specific, so we check that next + moreSpecific = false; + + // we check for a member of equal specificity by + // checking to see if this most specific member is + // explicitly more specific than the candidate + // member. if it is more specific, the candidate + // member can be chucked, otherwise we need to add + // the candidate member to the most-specific set + lessSpecific = + memberIsMoreSpecific(moreSpecificMember, member); + break; + } + } + + if (moreSpecific) { + // Member is the most specific now. + mostSpecificMembers.clear(); + mostSpecificMembers.add(member); + + } else if (! lessSpecific) { + // Add to ambiguity set if mutually unspecific. + mostSpecificMembers.add(member); + } + } + } + + if (mostSpecificMembers.size() > 1) { + throw new NoSuchMethodException( + "Ambiguous request for member in " + + clazz.getName() + + " matching given args" ); + } + + return (Member) mostSpecificMembers.get(0); + } + + public int hashCode () + { + return clazz.hashCode(); + } + + /** + * Loads up the data structures for my target class's constructors. + */ + private void maybeLoadConstructors () + { + if (ctorList == null) { + ctorList = new ArrayList(); + Constructor[] ctors = clazz.getConstructors(); + for (int i = 0; i < ctors.length; ++i) { + ctorList.add(ctors[i]); + paramMap.put(ctors[i], ctors[i].getParameterTypes()); + } + } + } + + /** + * Loads up the data structures for my target class's methods. + */ + private void maybeLoadMethods () + { + if (methodMap == null) { + methodMap = new HashMap(); + Method[] methods = clazz.getMethods(); + + for (int i = 0; i < methods.length; ++i) { + Method m = methods[i]; + String methodName = m.getName(); + Class[] paramTypes = m.getParameterTypes(); + + List list = (List) methodMap.get(methodName); + + if (list == null) { + list = new ArrayList(); + methodMap.put(methodName, list); + } + + if (! ClassUtil.classIsAccessible(clazz)) { + m = ClassUtil.getAccessibleMethodFrom( + clazz, methodName, paramTypes ); + } + + if (m != null) { + list.add(m); + paramMap.put(m, paramTypes); + } + } + } + } + + /** + * @param first a Member. + * @param second a Member. + * + * @return true if the first Member is more specific than the second, + * false otherwise. Specificity is determined according to the + * procedure in the Java Language Specification, section 15.12.2. + */ + private boolean memberIsMoreSpecific (Member first, Member second) + { + Class[] firstParamTypes = (Class[]) paramMap.get(first); + Class[] secondParamTypes = (Class[]) paramMap.get(second); + return ClassUtil.compatibleClasses( + secondParamTypes, firstParamTypes); + } + + /** + * The target class to look for methods and constructors in. + */ + private final Class clazz; + + /** + * Mapping from method name to the Methods in the target class with + * that name. + */ + private final Map methodMap = null; + + /** + * List of the Constructors in the target class. + */ + private final List ctorList = null; + + /** + * Mapping from a Constructor or Method object to the Class objects + * representing its formal parameters. + */ + private final Map paramMap = new HashMap(); +}