/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Michael Powers 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, see http://www.gnu.org */ package net.wotonomy.foundation.internal; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * This Introspector is a static utility class written to work around * limitations in PropertyDescriptor and Introspector.
*
* * Of particular note are the get() and set() methods, which will attempt to get * and set arbitrary values on arbitrary objects to the best of its ability, * converting values as appropriate. Properties of the form * "property.nestedproperty.anotherproperty" are supported to get and set values * on property values directly.
*
* * Note that for naming getter methods, this class supports "get", "is", and * also the property name itself, which supports NeXT-style properties. * Introspector supports Maps by treating the keys a property names, supports * Lists by treating the indexes as property names.
*
* * Numeric and boolean types can be inverted by prepending a "!" before the name * of the property, like "manager.!active" or "task.!lag". * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 893 $ */ public class Introspector { // allows "hasProperty" or "property" forms public static boolean strict = false; // print exception stack traces private static boolean debug = true; // path separator public static final String SEPARATOR = "."; // method cache - use hashtables for thread safety private static Map getterMethods = new Hashtable(); private static Map setterMethods = new Hashtable(); // wildcard value - using this class to represent a "wildcard" generic class. // we have to do this when matching methods by parameter types and a // null value is passed in - can't tell what class the null should be. public static Class WILD = Introspector.class; // empty class array - prevents having to create one every time private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; // use OGNL for property access private static boolean useOGNL; static { try { useOGNL = (Class.forName("ognl.Ognl") != null); } catch (ClassNotFoundException t) { useOGNL = false; } } /** * Utility method to get the read method for a property belonging to a class. * Will search for methods in the form of "getProperty" and failing that * "isProperty" (to handle booleans). * * @param objectClass the class whose property methods will be retrieved. * @param aProperty The property whose method will be retrieved. * @param paramTypes An array of class objects representing the types of * parameters. * @return The appropriate method for the class, or null if not found. */ static public Method getPropertyReadMethod(Class objectClass, String aProperty, Class[] paramTypes) { Method result = null; result = getMethodFromClass(objectClass, aProperty, paramTypes, true); return result; } /** * Utility method to get the write method for a property belonging to a class. * Will search for methods in the form of "setProperty". * * @param objectClass the class whose property methods will be retrieved. * @param aProperty The property whose method will be retrieved. * @param paramTypes An array of class objects representing the types of * parameters. * @return The appropriate method for the class, or null if not found. */ static public Method getPropertyWriteMethod(Class objectClass, String aProperty, Class[] paramTypes) { Method result = null; result = getMethodFromClass(objectClass, aProperty, paramTypes, false); return result; } /** * Gets a named method from a class. Using this method is preferred because the * results are cached and should be faster than calling Class.getMethod(). Note * that if an object has a "get" getter method and an "is" getter method with * the same signature defined for a given property. The "get" method is called. * * @param objectClass the Class whose property methods will be retrieved. * @param aMethodName A String containing the name of the desired method. * @param paramTypes An array of class objects representing the types of * parameters. * @return The appropriate Method from the Class, or null if not found. */ static private Method getMethodFromClass(Class objectClass, String aProperty, Class[] paramTypes, boolean doGetter) { // System.out.print( "Introspector.getMethodFromClass: " + aMethodName + " : " // ); Map classesToMethods = (doGetter ? getterMethods : setterMethods); Map allMethods = (Map) classesToMethods.get(objectClass); if (allMethods == null) { // need to build maps for this class mapPropertiesForClass(objectClass); // now the map should exist allMethods = (Map) classesToMethods.get(objectClass); } Method[] methods = (Method[]) allMethods.get(aProperty); if (methods == null) { return null; // property doesn't exist } methods_loop: // walks through all methods for name for (int i = 0; i < methods.length; i++) { Class[] types = methods[i].getParameterTypes(); // if parameter lengths don't match if (types.length != paramTypes.length) { // System.out.println( aMethodName + " : " + types.length + " != " + // paramTypes.length ); continue methods_loop; // continue with outer loop } // match up each parameter for (int j = 0; j < types.length; j++) { // convert primitives so they'll match - ugly // (would have thought isAssignableFrom() would catch this) if (types[j].isPrimitive()) { if (types[j] == Boolean.TYPE) { types[j] = Boolean.class; } else if (types[j] == Character.TYPE) { types[j] = Character.class; } else if (types[j] == Byte.TYPE) { types[j] = Byte.class; } else if (types[j] == Short.TYPE) { types[j] = Short.class; } else if (types[j] == Integer.TYPE) { types[j] = Integer.class; } else if (types[j] == Long.TYPE) { types[j] = Long.class; } else if (types[j] == Float.TYPE) { types[j] = Float.class; } else if (types[j] == Double.TYPE) { types[j] = Double.class; } } // if parameters don't match if ((paramTypes[j] != WILD) && (!types[j].isAssignableFrom(paramTypes[j]))) { // System.out.println( "Introspector.getMethodFromClass: " + // aProperty + " : " + types[j] + " != " + paramTypes[j] ); continue methods_loop; // continue with outer loop } } // all params match return methods[i]; } // no match return null; } static private final Method[] getAllMethodsForClass(Class aClass) { Method[] local = aClass.getDeclaredMethods(); // only local Method[] all = aClass.getMethods(); // all public Method[] result = new Method[local.length + all.length]; System.arraycopy(local, 0, result, 0, local.length); System.arraycopy(all, 0, result, local.length, all.length); return result; } /** * Generates a map of properties to both getter or setter methods for the given * class. Then assigned those maps into the appropriate getterMethods and * setterMethods maps keyed by the specified class. Even on error, this method * will at least place empty property maps into each of the methods maps. */ static private void mapPropertiesForClass(Class objectClass) { try { Map readProperties = new HashMap(); getterMethods.put(objectClass, readProperties); Map writeProperties = new HashMap(); setterMethods.put(objectClass, writeProperties); String name, property; Method[] methods = getAllMethodsForClass(objectClass); // throws SecurityException for (int i = 0; i < methods.length; i++) { name = methods[i].getName(); methods[i].setAccessible(true); // throws SecurityException if (name.startsWith("set")) { name = name.substring(3); if (!"".equals(name)) // excludes "set()" { putMethodIntoPropertyMap(name, methods[i], writeProperties); } } else if (methods[i].getReturnType() != void.class) { String fullname = name; if (name.startsWith("get")) { name = name.substring(3); } else if (name.startsWith("is")) { name = name.substring(2); } else if (name.startsWith("has") && (!strict)) // what about hashCode()? { name = name.substring(3); } if (!"".equals(name) && (!strict)) // excludes "get()", "has()", and "is()" { putMethodIntoPropertyMap(name, methods[i], readProperties); if (fullname != name) { // allows us to match properties that include the get/set prefix as well putMethodIntoPropertyMap(fullname, methods[i], readProperties); } } } } } catch (SecurityException se) { System.out.println("Introspector.getMethodFromClass: " + se); // this class will show up with empty getter/setter maps } } /** * Places a property-method pair into one of the properties maps. This in effect * maps a property to an array of methods. */ private static void putMethodIntoPropertyMap(String aProperty, Method aMethod, Map aMap) { // ensure first character is lower case StringBuffer buffer = new StringBuffer(aProperty); buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0))); String key = buffer.toString(); // build array of methods for property Method[] result = (Method[]) aMap.get(key); if (result == null) { result = new Method[] { aMethod }; } else { // create new array that's larger by one and copy int i; Method[] enlarged = new Method[result.length + 1]; for (i = 0; i < result.length; i++) { enlarged[i] = result[i]; } // add the new method to end enlarged[i] = aMethod; result = enlarged; } aMap.put(key, result); } /** * Utility method to get a method for a property belonging to a class. Use this * if you don't feel like making the Class array from the parameters you will be * using - pass in the parameters themselves. * * @param objectClass the Class whose property methods will be retrieved. * @param aProperty The property whose method will be retrieved. * @param params An array of parameters to be used. * @return The appropriate method for the class, or null if not found. */ static public Method getPropertyReadMethod(Class objectClass, String aProperty, Object[] params) { // optimization: avoid allocating class array for common case if (params.length == 0) { return getPropertyReadMethod(objectClass, aProperty, EMPTY_CLASS_ARRAY); } Class[] paramList = new Class[params.length]; for (int i = 0; i < params.length; i++) { if (params[i] != null) { paramList[i] = params[i].getClass(); } else { paramList[i] = WILD; } } return getPropertyReadMethod(objectClass, aProperty, paramList); } /** * Utility method to get a method for a property belonging to a class. Use this * if you don't feel like making the Class array from the parameters you will be * using - pass in the parameters themselves. * * @param objectClass the Class whose property methods will be retrieved. * @param aProperty The property whose method will be retrieved. * @param params An array of parameters to be used. * @return The appropriate method for the class, or null if not found. */ static public Method getPropertyWriteMethod(Class objectClass, String aProperty, Object[] params) { Class[] paramList = new Class[params.length]; for (int i = 0; i < params.length; i++) { if (params[i] != null) { paramList[i] = params[i].getClass(); } else { paramList[i] = WILD; } } return getPropertyWriteMethod(objectClass, aProperty, paramList); } /** * Gets a list of the readable properties for the given class. Note that * readable properties may not be writable - see getWriteProperties(). * * @return An array of property names in no particular order where each name is * a string with the first character in lower case. */ public static String[] getReadPropertiesForClass(Class objectClass) { Map properties = (Map) getterMethods.get(objectClass); if (properties == null) { // need to build maps for this class mapPropertiesForClass(objectClass); // now the map should exist properties = (Map) getterMethods.get(objectClass); } // put property names into string array Set keys = properties.keySet(); Iterator it = keys.iterator(); int len = keys.size(); String[] result = new String[len]; for (int i = 0; i < len; i++) { result[i] = (String) it.next(); } return result; } /** * Gets a list of the writable properties for the given class. Note that * writable properties may not be writable - see getReadProperties(). * * @return An array of property names in no particular order where each name is * a string with the first character in lower case. */ public static String[] getWritePropertiesForClass(Class objectClass) { Map properties = (Map) setterMethods.get(objectClass); if (properties == null) { // need to build maps for this class mapPropertiesForClass(objectClass); // now the map should exist properties = (Map) setterMethods.get(objectClass); } // put property names into string array Set keys = properties.keySet(); Iterator it = keys.iterator(); int len = keys.size(); String[] result = new String[len]; for (int i = 0; i < len; i++) { result[i] = (String) it.next(); } return result; } /** * Gets a list of the readable properties for the given object, which may not be * null. This method is more useful than getReadPropertiesForClass in that Maps * will return their keys as properties and Lists will return their element * indices as properties. Note that readable properties may not be writable - * see getWriteProperties(). * * @return An array of property names in no particular order where each name is * a string with the first character in lower case. */ public static String[] getReadPropertiesForObject(Object anObject) { List properties = new ArrayList(); String[] classProperties = getReadPropertiesForClass(anObject.getClass()); if (anObject instanceof List) { properties.addAll(getPropertiesForList((List) anObject)); } if (anObject instanceof Map) { properties.addAll(getPropertiesForMap((Map) anObject)); } int i; int len = classProperties.length + properties.size(); String[] result = new String[len]; for (i = 0; i < classProperties.length; i++) { result[i] = classProperties[i]; } Iterator it = properties.iterator(); while (it.hasNext()) { result[i++] = it.next().toString(); } return result; } /** * Gets a list of the writable properties for the given object, which may not be * null. This method is more useful than getWritePropertiesForClass in that Maps * will return their keys as properties and Lists will return their element * indices as properties. Note that writable properties may not be writable - * see getReadProperties(). * * @return An array of property names in no particular order where each name is * a string with the first character in lower case. */ public static String[] getWritePropertiesForObject(Object anObject) { List properties = new ArrayList(); String[] classProperties = getWritePropertiesForClass(anObject.getClass()); if (anObject instanceof List) { properties.addAll(getPropertiesForList((List) anObject)); } if (anObject instanceof Map) { properties.addAll(getPropertiesForMap((Map) anObject)); } int i; int len = classProperties.length + properties.size(); String[] result = new String[len]; for (i = 0; i < classProperties.length; i++) { result[i] = classProperties[i]; } Iterator it = properties.iterator(); while (it.hasNext()) { result[i++] = it.next().toString(); } return result; } private static List getPropertiesForList(List aList) { List result = new ArrayList(); int len = aList.size(); for (int i = 0; i < len; i++) { result.add(new Integer(i).toString()); } return result; } private static List getPropertiesForMap(Map aMap) { List result = new ArrayList(); Iterator it = ((Map) aMap).keySet().iterator(); while (it.hasNext()) { result.add(it.next().toString()); } return result; } private static Object[] EMPTY_ARRAY = new Object[0]; /** * Convenience to get a value for a property from an object. An empty property * string is considered the identity property and simply returns the object. * * @throws MissingPropertyException if the property cannot be found on the * object. */ public static Object getValueForObject(Object anObject, String aProperty) { if ((aProperty == null) || ("".equals(aProperty))) { return anObject; } if (useOGNL && aProperty.startsWith("ognl:")) { try { return ognl.Ognl.getValue(aProperty, anObject); } catch (Throwable t) { if (debug) { System.err.println("Introspector.getValueForObject: " + anObject + "' ( " + anObject.getClass() + " )" + ", ognl:" + aProperty); System.err.println(t); } return null; } } boolean invert = false; if (aProperty.startsWith("!")) { aProperty = aProperty.substring(1); invert = true; } Object result = null; try { Method m = Introspector.getPropertyReadMethod(anObject.getClass(), aProperty, EMPTY_ARRAY); if (m != null) { result = m.invoke(anObject, EMPTY_ARRAY); } else // no method, try for field { try { Field field = anObject.getClass().getDeclaredField(aProperty); if (field != null) { field.setAccessible(true); // throws SecurityException result = field.get(anObject); } } catch (Throwable t) { // ignore for now } } if (result == null) { if (anObject instanceof Map) { result = ((Map) anObject).get(aProperty); } else if (anObject instanceof List) { result = ((List) anObject).get(Integer.parseInt(aProperty)); } } if (invert) { Object inverted = ValueConverter.invert(result); if (inverted != null) result = inverted; } // System.out.println( "getValueForObject: " + anObject + " : " + aProperty + " // : " + result ); return result; } catch (Throwable exc) { if (exc instanceof InvocationTargetException) { exc = ((InvocationTargetException) exc).getTargetException(); } if (exc instanceof RuntimeException) { throw (RuntimeException) exc; } if (debug) { System.out.println("Introspector.getValueForObject: " + anObject + "' ( " + anObject.getClass() + " )" + ", " + aProperty + ": "); } throw new WotonomyException(exc); } //! throw new MissingPropertyException(); } /** * Convenience to set a value for a property from an object. Returns the return * value from executing the specified method, or null if the method returns type * void. * * @throws MissingPropertyException if the property cannot be found on the * object. * @throws NullPrimitiveException if the property is of primitive type and the * value is null. */ public static Object setValueForObject(Object anObject, String aProperty, Object aValue) { if (useOGNL && aProperty.startsWith("ognl:")) { try { ognl.Ognl.setValue(aProperty, anObject, aValue); } catch (Throwable t) { if (debug) { System.err.println("Introspector.setValueForObject: " + anObject + "' ( " + anObject.getClass() + " )" + ", ognl:" + aProperty + " : " + aValue); System.err.println(t); } } return null; } try { if (aProperty.startsWith("!")) { aProperty = aProperty.substring(1); Object inverted = ValueConverter.invert(aValue); if (inverted != null) aValue = inverted; } Method m = null; if (aValue != null) { m = Introspector.getPropertyWriteMethod(anObject.getClass(), aProperty, new Class[] { aValue.getClass() }); } if (m == null) { m = Introspector.getPropertyWriteMethod(anObject.getClass(), aProperty, new Class[] { WILD }); if ((m != null) && (aValue != null)) { // check for null primitive if ((aValue == null) && (m.getParameterTypes()[0].isPrimitive())) { throw new NullPrimitiveException(); } // convert if possible Object o = ValueConverter.convertObjectToClass(aValue, m.getParameterTypes()[0]); if (o != null) { aValue = o; } } } if (m != null) { return m.invoke(anObject, new Object[] { aValue }); } else // no method, try for field { try { Field field = anObject.getClass().getDeclaredField(aProperty); if (field != null) { field.setAccessible(true); // throws SecurityException field.set(anObject, aValue); return null; } } catch (Throwable t) { // ignore for now } } if (anObject instanceof Map) { return ((Map) anObject).put(aProperty, aValue); } if (anObject instanceof List) { List list = (List) anObject; int i = Integer.parseInt(aProperty); if (list.size() < i + 1) { // expand list as necessary for (int j = list.size(); j <= i; j++) { list.add(new Object()); // placeholder } } return list.set(i, aValue); } } catch (Throwable exc) { if (exc instanceof IllegalArgumentException) { System.out.println( "Introspector.setValueForObject: " + anObject + " , " + aProperty + " , '" + aValue + "' ):"); System.out.println(exc); } else if (exc instanceof InvocationTargetException) { exc = ((InvocationTargetException) exc).getTargetException(); } if (exc instanceof RuntimeException) { throw (RuntimeException) exc; } if (debug) { System.out.println( "Introspector.setValueForObject: " + anObject + " , " + aProperty + " , '" + aValue + "' ):"); } throw new WotonomyException(exc); } return null; //! throw new MissingPropertyException(); } /** * Gets a value from an object or any of its child objects. This will parse the * property string for "."'s and get values for each successive object's * property in the path. An empty property string is considered the identity * property and simply returns the object. */ public static Object get(Object anObject, String aProperty) { int i = aProperty.indexOf(SEPARATOR); if (i == -1) return getValueForObject(anObject, aProperty); String pathElement = aProperty.substring(0, i); String remainder = aProperty.substring(i + 1); Object result = getValueForObject(anObject, pathElement); if (result == null) return null; return get(result, remainder); } /** * Sets a value in an object or any of its child objects. This will parse the * property string for "."'s and set values for each successive object's * property in the path.
*
* * If a property is not found, this method will try to implicitly create hash * maps (if possible) to fill out the path. This is useful when dealing with * trees of nested maps. */ public static Object set(Object anObject, String aProperty, Object aValue) { int i = aProperty.indexOf(SEPARATOR); if (i == -1) return setValueForObject(anObject, aProperty, aValue); String pathElement = aProperty.substring(0, i); String remainder = aProperty.substring(i + 1); Object result = getValueForObject(anObject, pathElement); if (result == null) { result = new HashMap(2); setValueForObject(anObject, pathElement, result); } return set(result, remainder, aValue); } /** * If set to true, exceptions printed to System.out.println. Defaults to true. */ public void setDebug(boolean isDebug) { debug = isDebug; } } /* * $Log$ Revision 1.2 2006/02/16 13:11:47 cgruber Check in all sources in * eclipse-friendly maven-enabled packages. * * Revision 1.19 2004/02/05 02:20:34 mpowers Added experimental ognl support (if * ognl is present). * * Revision 1.18 2003/03/26 16:44:35 mpowers Now correctly reflecting on all * methods, not just locally declared ones. * * Revision 1.17 2003/02/21 21:10:51 mpowers Now reaching package, protected, * and private methods and fields. * * Revision 1.16 2003/01/28 22:11:59 mpowers Now more lenient in resolving * properties starting with "is" "get" or "has". * * Revision 1.15 2003/01/27 15:10:54 mpowers Better handling for illegal * argument exceptions. * * Revision 1.14 2003/01/18 23:30:42 mpowers WODisplayGroup now compiles. * * Revision 1.13 2002/10/11 15:35:12 mpowers Removed printlns. * * Revision 1.11 2001/05/02 17:58:41 mpowers Removed debugging code, added * comments. * * Revision 1.10 2001/04/08 21:00:54 mpowers Changes to support new * objectsForFetchSpecification scheme. * * Revision 1.9 2001/03/29 03:30:36 mpowers Refactored duplicator a bit. * Disabled MissingPropertyExceptions for now. * * Revision 1.8 2001/03/28 17:52:45 mpowers Corrected the throws in the docs. * * Revision 1.7 2001/03/28 17:49:13 mpowers Better exception handling in * Introspector. * * Revision 1.6 2001/03/13 21:40:20 mpowers Improved handling of runtime * exceptions. * * Revision 1.5 2001/03/09 22:06:35 mpowers Now extracting the wrapped exception * from InvocationTargetExceptions. * * Revision 1.4 2001/03/01 20:36:35 mpowers Better error handling and better * handling of nulls. * * Revision 1.3 2001/01/17 16:20:57 mpowers Introspector now handles the * identity property. * * Revision 1.2 2001/01/09 20:08:17 mpowers Slight optimization. * * Revision 1.1.1.1 2000/12/21 15:52:04 mpowers Contributing wotonomy. * * Revision 1.5 2000/12/20 16:25:46 michael Added log to all files. * * */