/*
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.
*
*
*/