/*
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.util;
import java.lang.reflect.*;
import java.util.*; //collections
/**
* This Introspector is a static utility class written to work
* around limitations in PropertyDescriptor and Introspector.<br><br>
*
* Of particular note are the get() and set() methods, which will attempt
* to get and set artibrary 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.<br><br>
*
* 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. <br><br>
*
* 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: mpowers $
* @version $Revision: 1.19 $
*/
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;
String methodName = 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;
String methodName = 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.<br><br>
*
* 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: Introspector.java,v $
* 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.
*
*
*/