/*
* Copyright 2013 Guidewire Software, Inc.
*/
package gw.internal.gosu.parser;
import gw.lang.reflect.gs.IGosuObject;
import gw.util.concurrent.LockingLazyVar;
import java.beans.BeanDescriptor;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.beans.PropertyVetoException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* Adapted from java.beans.Introspector. Fixes a bug involving covariant return
* types (jdk 1.5) where a subtype's method is hidden by the super's method.
*/
public class NewIntrospector
{
// Static Caches to speed up introspection.
private static final Map DECLARED_METHOD_CACHE = new WeakHashMap( 100 );
private static final Object DECLARED_METHODS_LOCK = new Object();
private static final ConcurrentHashMap<Class, GenericBeanInfo> BEAN_INFO_CACHE = new ConcurrentHashMap<Class, GenericBeanInfo>();
private static final String GET_PREFIX = "get";
private static final String SET_PREFIX = "set";
private static final String IS_PREFIX = "is";
private Class beanClass;
// Methods maps from Method objects to MethodDescriptors
private Map<String, GWMethodDescriptor> methods = new LinkedHashMap<String, GWMethodDescriptor>();
// properties maps from String names to PropertyDescriptors
private Map<String, PropertyDescriptor> properties = new TreeMap<String, PropertyDescriptor>();
private HashMap<String, List<PropertyDescriptor>> pdStore;
private static LockingLazyVar<DeclaredMethodsAccessor> _declaredMethodsAccessor = new LockingLazyVar<DeclaredMethodsAccessor>() {
@Override
protected DeclaredMethodsAccessor init() {
Boolean result = (Boolean)AccessController.doPrivileged( new PrivilegedAction()
{
public Object run()
{
try
{
Method m = Class.class.getDeclaredMethod( "privateGetDeclaredMethods", boolean.class );
return true;
}
catch( Exception e )
{
return false;
}
}
});
if (Boolean.TRUE.equals(result)) {
return new PrivateGetDeclaredMethodsAccessor();
} else {
return new PublicGetDeclaredMethodsAccessor();
}
}
};
/**
* Introspect on a Java bean and learn all about its properties, exposed
* methods, below a given "stop" point.
* <p/>
* If the BeanInfo class for a Java Bean has been previously Introspected
* based on the same arguments, then the BeanInfo class is retrieved
* from the BeanInfo cache.
*
*
* @param beanClass The bean class to be analyzed.
*
* @throws java.beans.IntrospectionException
* if an exception occurs during
* introspection.
*/
public static GenericBeanInfo getBeanInfo(Class beanClass)
{
GenericBeanInfo bi = BEAN_INFO_CACHE.get(beanClass);
if (bi == null) {
try {
bi = (new NewIntrospector( beanClass )).getBeanInfo();
BEAN_INFO_CACHE.put(beanClass, bi);
} catch (IntrospectionException e) {
throw new RuntimeException(e);
}
}
return bi;
// Old behaviour: Make an independent copy of the BeanInfo.
//return new GenericBeanInfo(bi);
}
/**
*/
public static String capitalizeFirstChar( String name )
{
if( name == null || name.length() == 0 )
{
return name;
}
if( name.startsWith( "_" ) )
{
return capitalizeFirstChar( name.substring( 1 ) );
}
char chars[] = name.toCharArray();
chars[0] = Character.toUpperCase( chars[0] );
return new String( chars );
}
/**
* Flush all of the Introspector's internal caches. This method is
* not normally required. It is normally only needed by advanced
* tools that update existing "Class" objects in-place and need
* to make the Introspector re-analyze existing Class objects.
*/
public static void flushCaches()
{
DECLARED_METHOD_CACHE.clear();
BEAN_INFO_CACHE.clear();
}
//======================================================================
// Private implementation methods
//======================================================================
private NewIntrospector( Class beanClass )
throws IntrospectionException
{
this.beanClass = beanClass;
}
/**
* Constructs a GenericBeanInfo class from the state of the Introspector
*/
private GenericBeanInfo getBeanInfo() throws IntrospectionException
{
// the evaluation order here is import, as we evaluate the
// event sets and locate PropertyChangeListeners before we
// look for properties.
BeanDescriptor bd = getTargetBeanDescriptor();
GWMethodDescriptor mds[] = getTargetMethodInfo();
PropertyDescriptor pds[] = getTargetPropertyInfo();
return new GenericBeanInfo( bd, pds, mds );
}
/**
* @return An array of PropertyDescriptors describing the editable
* properties supported by the target bean.
*/
private PropertyDescriptor[] getTargetPropertyInfo() throws IntrospectionException
{
// Apply some reflection to the current class.
getPropertiesFromMethods();
processPropertyDescriptors();
// Allocate and populate the result array.
PropertyDescriptor result[] = new PropertyDescriptor[properties.size()];
result = properties.values().toArray( result );
return result;
}
private void getPropertiesFromMethods()
{
// First get an array of all the public methods at this level
Method methodList[] = getDeclaredMethods(beanClass);
// Now analyze each method.
for( Method method : methodList )
{
if( method.isSynthetic() )
{
continue;
}
String name = method.getName();
Class argTypes[] = method.getParameterTypes();
Class resultType = method.getReturnType();
int argCount = argTypes.length;
PropertyDescriptor pd = null;
try
{
if( argCount == 0 )
{
if( name.startsWith( GET_PREFIX ) )
{
// Simple getter
pd = createPropertyDescriptor( capitalizeFirstChar( name.substring( 3 ) ),
method, null );
}
else if( name.startsWith( IS_PREFIX ) && (resultType == boolean.class || resultType == Boolean.class) )
{
// Boolean getter
pd = createPropertyDescriptor( capitalizeFirstChar( name.substring( 2 ) ), method, null );
}
}
else if( argCount == 1 )
{
// Simple setter
if( resultType == void.class && name.startsWith( SET_PREFIX ) )
{
pd = new PropertyDescriptor( capitalizeFirstChar( name.substring( 3 ) ),
null, method );
if( throwsException( method, PropertyVetoException.class ) )
{
pd.setConstrained( true );
}
}
}
}
catch( IntrospectionException ex )
{
// This happens if a PropertyDescriptor
// constructor fins that the method violates details of the deisgn
// pattern, e.g. by having an empty name, or a getter returning
// void , or whatever.
pd = null;
}
if( pd != null )
{
addPropertyDescriptor( pd );
}
}
}
/**
* Adds the property descriptor to the list store.
*/
private void addPropertyDescriptor( PropertyDescriptor pd )
{
if( pdStore == null )
{
pdStore = new HashMap<String, List<PropertyDescriptor>>();
}
String propName = pd.getName();
List<PropertyDescriptor> list = pdStore.get( propName );
if( list == null )
{
list = new ArrayList<PropertyDescriptor>();
pdStore.put( propName, list );
}
for (int i = 0; i < list.size(); i++) {
PropertyDescriptor otherPd = list.get(i);
if (pd.getReadMethod() != null && otherPd.getReadMethod() != null) {
if (pd.getReadMethod().getName().startsWith("is") && otherPd.getReadMethod().getName().startsWith("get")) {
// do not overwrite a getter with an is
return;
}
else if (pd.getReadMethod().getName().startsWith("get") && otherPd.getReadMethod().getName().startsWith("is")) {
// overwrite boolean is with get.
list.remove(i);
break;
}
}
}
list.add( pd );
}
/**
* Populates the property descriptor table by merging the
* lists of Property descriptors.
*/
private void processPropertyDescriptors() throws IntrospectionException
{
if( pdStore == null )
{
return;
}
List<PropertyDescriptor> list;
PropertyDescriptor pd, gpd, spd;
for( List<PropertyDescriptor> propertyDescriptors : pdStore.values() )
{
pd = null;
gpd = null;
spd = null;
list = propertyDescriptors;
// First pass. Find the latest getter method. Merge properties
// of previous getter methods.
for( Object aList1 : list )
{
pd = (PropertyDescriptor)aList1;
if( pd.getReadMethod() != null && !pd.getReadMethod().isSynthetic() )
{
gpd = pd;
}
}
// Second pass. Find the latest setter method which
// has the same type as the getter method.
for( Object aList : list )
{
pd = (PropertyDescriptor)aList;
if( pd.getWriteMethod() != null && !pd.getWriteMethod().isSynthetic() )
{
if( gpd != null )
{
if( gpd.getPropertyType() == pd.getPropertyType() )
{
spd = pd;
}
}
else
{
spd = pd;
}
}
}
// At this stage we should have either PDs or IPDs for the
// representative getters and setters. The order at which the
// property decrriptors are determined represent the
// precedence of the property ordering.
pd = null;
if( gpd != null && spd != null )
{
pd = createPropertyDescriptor(gpd.getName(), gpd.getReadMethod(), spd.getWriteMethod());
}
else if( spd != null )
{
Method getterFromSuper = findGetterInSuper( beanClass, spd );
if( getterFromSuper != null )
{
pd = createPropertyDescriptor( spd.getName(), getterFromSuper, spd.getWriteMethod() );
}
else
{
// simple setter
pd = spd;
}
}
else if( gpd != null )
{
Method setterFromSuper = findSetterInSuper( beanClass, gpd );
if( setterFromSuper != null )
{
pd = createPropertyDescriptor( gpd.getName(), gpd.getReadMethod(), setterFromSuper );
}
else
{
// simple getter
pd = gpd;
}
}
if( pd != null )
{
properties.put( pd.getName(), pd );
}
}
}
private PropertyDescriptor createPropertyDescriptor(String name, Method readMethod, Method writeMethod) throws IntrospectionException {
PropertyDescriptor pd;
try
{
pd = new PropertyDescriptor( name, readMethod, writeMethod );
}
catch( IntrospectionException ie )
{
pd = new PropertyDescriptor( name, Object.class, null, null );
// Handle case where the JVM checks whether or not the setter/getter is public (eg. websphere)
try {
createPropertyDescriptorForNonPublicMethods(readMethod, writeMethod, pd);
} catch( NoSuchFieldException nsfe ) {
pd = null;
} catch (IllegalAccessException e) {
pd = null;
}
}
return pd;
}
private void createPropertyDescriptorForNonPublicMethods(Method readMethod, Method writeMethod, PropertyDescriptor pd) throws NoSuchFieldException, IllegalAccessException {
Field field = PropertyDescriptor.class.getDeclaredField("getter");
field.setAccessible( true );
field.set( pd, readMethod );
field = PropertyDescriptor.class.getDeclaredField("setter");
field.setAccessible( true );
field.set( pd, writeMethod );
}
private Method findSetterInSuper( Class<?> type, PropertyDescriptor gpd )
{
if( type == null )
{
return null;
}
boolean bStatic = Modifier.isStatic( gpd.getReadMethod().getModifiers() );
Method setter = null;
Method[] methods = getDeclaredMethods( type );
for( int i = 0; i < methods.length; i++ )
{
Method m = methods[i];
if( m.isSynthetic() || Modifier.isStatic( m.getModifiers() ) != bStatic )
{
continue;
}
if( m.getName().equals( SET_PREFIX + gpd.getName() ) )
{
//!! Note PropertyDescriptor enforces that the get and set types must match exactly -- it does not support covariance. Figures.
// if( m.getParameterTypes().length == 1 &&
// m.getParameterTypes()[0].isAssignableFrom( gpd.getPropertyType() ) )
// {
// if( setter == null || setter.getParameterTypes()[0].isAssignableFrom( m.getParameterTypes()[0] ) )
// {
// setter = m;
// }
// }
if( m.getParameterTypes().length == 1 && m.getParameterTypes()[0] == gpd.getPropertyType() )
{
setter = m;
break;
}
}
}
if( setter == null )
{
setter = findSetterInSuper(type.getSuperclass(), gpd);
if( setter == null && (type.isInterface() || Modifier.isAbstract( type.getModifiers() )) )
{
for( Class iface : type.getInterfaces() )
{
setter = findSetterInSuper(iface, gpd);
}
}
}
return setter;
}
private Method findGetterInSuper( Class<?> type, PropertyDescriptor spd )
{
if( type == null )
{
return null;
}
boolean bStatic = Modifier.isStatic( spd.getWriteMethod().getModifiers() );
Method getter = null;
Method[] methods = getDeclaredMethods( type );
for( int i = 0; i < methods.length; i++ )
{
Method m = methods[i];
if( m.isSynthetic() || Modifier.isStatic( m.getModifiers() ) != bStatic )
{
continue;
}
if( m.getName().equals( GET_PREFIX + spd.getName() ) )
{
if( m.getParameterTypes().length == 0 && m.getReturnType() == spd.getPropertyType() )
{
getter = m;
break;
}
}
}
if( getter == null )
{
getter = findGetterInSuper( type.getSuperclass(), spd );
if( getter == null && (type.isInterface() || Modifier.isAbstract( type.getModifiers() )) )
{
for( Class iface : type.getInterfaces() )
{
getter = findGetterInSuper( iface, spd );
}
}
}
return getter;
}
/**
* @return An array of MethodDescriptors describing the private
* methods supported by the target bean.
*/
private GWMethodDescriptor[] getTargetMethodInfo() throws IntrospectionException
{
// First get an array of all the beans methods at this level
Method methodList[] = getDeclaredMethods( beanClass );
// Now analyze each method.
for( Method method : methodList )
{
if( method.isSynthetic() )
{
continue;
}
GWMethodDescriptor md = new GWMethodDescriptor( method );
addMethod( md );
}
// Allocate and populate the result array.
GWMethodDescriptor result[] = new GWMethodDescriptor[methods.size()];
result = methods.values().toArray( result );
return result;
}
private void addMethod( GWMethodDescriptor md )
{
// We have to be careful here to distinguish method by both name
// and argument lists.
// This method gets called a *lot, so we try to be efficient.
String name = md.getName();
GWMethodDescriptor old = methods.get(name);
if( old == null )
{
// This is the common case.
methods.put( name, md );
return;
}
// We have a collision on method names. This is rare.
// Check if old and md have the same type.
Class p1[] = md.getMethod().getParameterTypes();
Class p2[] = old.getMethod().getParameterTypes();
boolean match = false;
if( p1.length == p2.length )
{
match = true;
for( int i = 0; i < p1.length; i++ )
{
if( p1[i] != p2[i] )
{
match = false;
break;
}
}
}
if( match )
{
methods.put( name, md );
return;
}
// We have a collision on method names with different type signatures.
// This is very rare.
String longKey = makeQualifiedMethodName( md );
methods.put( longKey, md );
}
private String makeQualifiedMethodName( GWMethodDescriptor md )
{
Method m = md.getMethod();
StringBuilder sb = new StringBuilder();
sb.append( m.getName() );
sb.append( '=' );
Class params[] = m.getParameterTypes();
for( Class param : params )
{
sb.append( ':' );
sb.append( param.getName() );
}
return sb.toString();
}
private BeanDescriptor getTargetBeanDescriptor()
{
// OK, fabricate a default BeanDescriptor.
return (new BeanDescriptor( beanClass ));
}
public static Method[] getDeclaredMethods( Class clz )
{
// For anything within the GosuClassLoader, we want to make sure to
if ( IGosuObject.class.isAssignableFrom( clz ) ) {
return clz.getDeclaredMethods();
}
// Looking up Class.getDeclaredMethods is relatively expensive,
// so we cache the results.
Method[] result;
final Class fclz = clz;
synchronized (DECLARED_METHODS_LOCK) {
result = (Method[])DECLARED_METHOD_CACHE.get( fclz );
if( result != null )
{
return result;
}
}
result = _declaredMethodsAccessor.get().getDeclaredMethods(clz);
Arrays.sort(result, new Comparator<Method>() {
public int compare(Method o1, Method o2) {
int res = o1.getName().compareTo(o2.getName());
if (res == 0) {
// We want bridge methods to be the last ones. They have less concrete return types.
boolean b1 = o1.isBridge();
boolean b2 = o2.isBridge();
if (b1 != b2)
{
res = b1 ? 1 : -1;
}
}
return res;
}
});
// Add it to the cache.
synchronized (DECLARED_METHODS_LOCK) {
DECLARED_METHOD_CACHE.put( fclz, result );
}
return result;
}
private static interface DeclaredMethodsAccessor {
Method[] getDeclaredMethods(Class clz);
}
private static class PrivateGetDeclaredMethodsAccessor implements DeclaredMethodsAccessor {
@Override
public Method[] getDeclaredMethods(final Class clz) {
Method[] result = (Method[])AccessController.doPrivileged( new PrivilegedAction()
{
public Object run()
{
try
{
Method m = Class.class.getDeclaredMethod( "privateGetDeclaredMethods", boolean.class );
m.setAccessible( true );
return m.invoke( clz, false );
}
catch( Exception e )
{
System.err.println("WARNING Cannot load methods of " + clz.getName() + ": " + getRootCause(e).toString());
return new Method[0];
// throw new RuntimeException( e );
}
// return fclz.getDeclaredMethods();
}
} );
Method[] copy = new Method[result.length]; // copy so as not to mess up the Class' method offsets
System.arraycopy( result, 0, copy, 0, copy.length );
result = copy;
return result;
}
}
/**
* Traverse exception chain to return the root cause.
*
* @param throwable The top-level exception in the chain
* @return The root (or top-level if none chained)
*/
public static Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable;
while (cause.getCause() != null) {
cause = cause.getCause();
}
return cause;
}
public static class PublicGetDeclaredMethodsAccessor implements DeclaredMethodsAccessor {
@Override
public Method[] getDeclaredMethods(Class clz) {
return clz.getDeclaredMethods();
}
}
//======================================================================
// Package private support methods.
//======================================================================
/**
* Return true iff the given method throws the given exception.
*/
private boolean throwsException( Method method, Class exception )
{
Class exs[] = method.getExceptionTypes();
for( Class ex : exs )
{
if( ex == exception )
{
return true;
}
}
return false;
}
} // end class Introspector