/*************************************************************************
*
* ADOBE CONFIDENTIAL
* __________________
*
* Copyright 2002 - 2007 Adobe Systems Incorporated
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated
* and its suppliers and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package flex.messaging.io;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import flex.messaging.MessageException;
import flex.messaging.io.amf.ASObject;
import flex.messaging.log.Log;
import flex.messaging.log.Logger;
import flex.messaging.util.ExceptionUtil;
/**
* Uses Bean introspection to collect the properties for a given instance.
*
* @author Peter Farland
*/
public class BeanProxy extends AbstractProxy
{
static final long serialVersionUID = 7365078101695257715L;
private static final int FAILED_PROPERTY_READ_ERROR = 10021;
private static final int FAILED_PROPERTY_WRITE_ERROR = 10022;
private static final int NON_READABLE_PROPERTY_ERROR = 10023;
private static final int NON_WRITABLE_PROPERTY_ERROR = 10024;
private static final int UNKNOWN_PROPERTY_ERROR = 10025;
protected static final Map propertyNamesCache = new IdentityHashMap();
protected static final Map beanPropertyCache = new IdentityHashMap();
protected static final Map propertyDescriptorCache = new IdentityHashMap();
protected boolean cacheProperties = true;
protected boolean cachePropertiesDescriptors = true;
protected Class stopClass = Object.class;
protected static final Map ignoreProperties = new HashMap();
static
{
addIgnoreProperty(AbstractMap.class, "empty");
addIgnoreProperty(AbstractCollection.class, "empty");
addIgnoreProperty(ASObject.class, "type");
addIgnoreProperty(Throwable.class, "stackTrace");
addIgnoreProperty(File.class, "parentFile");
addIgnoreProperty(File.class, "canonicalFile");
addIgnoreProperty(File.class, "absoluteFile");
}
/**
* Constructor
*/
public BeanProxy()
{
this(null);
}
/**
* Construct a new BeanProxy with the provided default instance.
*
* @param defaultInstance defines the alias if provided
*/
public BeanProxy(Object defaultInstance)
{
super(defaultInstance);
// Override default behavior here... standard Map implementations
// are treated as anonymous Objects, i.e. without an alias.
if (defaultInstance != null)
{
alias = getClassName(defaultInstance);
}
}
/** {@inheritDoc} */
public String getAlias(Object instance)
{
return getClassName(instance);
}
/** {@inheritDoc} */
public List getPropertyNames(Object instance)
{
if (instance == null)
return null;
Class c = instance.getClass();
List propertyNames = null;
Map properties;
if (descriptor == null)
{
propertyNames = (List)propertyNamesCache.get(c);
}
if (propertyNames != null)
{
return propertyNames;
}
else
{
properties = getBeanProperties(instance);
// FIXME: Pete, This list could be generated at the time the bean properties
// are collected.
propertyNames = new ArrayList(properties.size());
Iterator it = properties.keySet().iterator();
while (it.hasNext())
{
propertyNames.add(it.next().toString());
}
if (cacheProperties && descriptor == null)
{
synchronized(propertyNamesCache)
{
List propertyNames2 = (List)propertyNamesCache.get(c);
if (propertyNames2 == null)
propertyNamesCache.put(c, propertyNames);
else
propertyNames = propertyNames2;
}
}
}
return propertyNames;
}
/** {@inheritDoc} */
public Class getType(Object instance, String propertyName)
{
if (instance == null || propertyName == null)
return null;
BeanProperty bp = getBeanProperty(instance, propertyName);
if (bp != null)
{
return bp.getType();
}
return null;
}
/** {@inheritDoc} */
public Object getValue(Object instance, String propertyName)
{
if (instance == null || propertyName == null)
return null;
BeanProperty bp = getBeanProperty(instance, propertyName);
if (bp != null)
{
return getBeanValue(instance, bp);
}
else
{
SerializationContext context = getSerializationContext();
if (!ignorePropertyErrors(context))
{
// Property '{propertyName}' not found on class '{alias}'.
MessageException ex = new MessageException();
ex.setMessage(UNKNOWN_PROPERTY_ERROR, new Object[] {propertyName, getAlias(instance)});
throw ex;
}
}
return null;
}
/**
* Gets the value specified by the BeanProperty
* @param instance Object to get the value from
* @param bp the property to get
* @return the value of the property if it exists
*/
protected final Object getBeanValue(Object instance, BeanProperty bp)
{
String propertyName = bp.getName();
if (bp.isRead())
{
try
{
Object value = bp.get(instance);
if (value != null && descriptor != null)
{
SerializationDescriptor subDescriptor = (SerializationDescriptor)descriptor.get(propertyName);
if (subDescriptor != null)
{
PropertyProxy subProxy = PropertyProxyRegistry.getProxyAndRegister(value);
subProxy = (PropertyProxy)subProxy.clone();
subProxy.setDescriptor(subDescriptor);
subProxy.setDefaultInstance(value);
value = subProxy;
}
}
return value;
}
catch (Exception e)
{
SerializationContext context = getSerializationContext();
// Log failed property set errors
if (Log.isWarn() && logPropertyErrors(context))
{
Logger log = Log.getLogger(LOG_CATEGORY);
log.warn("Failed to get property {0} on type {1}.",
new Object[] {propertyName, getAlias(instance)}, e);
}
if (!ignorePropertyErrors(context))
{
// Failed to get property '{propertyName}' on type '{className}'.
MessageException ex = new MessageException();
ex.setMessage(FAILED_PROPERTY_READ_ERROR, new Object[] {propertyName, getAlias(instance)});
ex.setRootCause(e);
throw ex;
}
}
}
else
{
SerializationContext context = getSerializationContext();
if (!ignorePropertyErrors(context))
{
//Property '{propertyName}' not readable from class '{alias}'.
MessageException ex = new MessageException();
ex.setMessage(NON_READABLE_PROPERTY_ERROR, new Object[] {propertyName, getAlias(instance)});
throw ex;
}
}
return null;
}
/** {@inheritDoc} */
public void setValue(Object instance, String propertyName, Object value)
{
BeanProperty bp = getBeanProperty(instance, propertyName);
if (bp != null)
{
if (bp.isWrite())
{
try
{
Class desiredPropClass = bp.getType();
TypeMarshaller marshaller = TypeMarshallingContext.getTypeMarshaller();
value = marshaller.convert(value, desiredPropClass);
bp.set(instance, value);
}
catch (Exception e)
{
SerializationContext context = getSerializationContext();
// Log ignore failed property set errors
if (Log.isWarn() && logPropertyErrors(context))
{
Logger log = Log.getLogger(LOG_CATEGORY);
log.warn("Failed to set property {0} on type {1}.",
new Object[] {propertyName, getAlias(instance)}, e);
}
if (!ignorePropertyErrors(context))
{
// Failed to get property '{propertyName}' on type '{className}'.
MessageException ex = new MessageException();
ex.setMessage(FAILED_PROPERTY_WRITE_ERROR, new Object[] {propertyName, getAlias(instance)});
ex.setRootCause(e);
throw ex;
}
}
}
else
{
SerializationContext context = getSerializationContext();
if (Log.isWarn() && logPropertyErrors(context))
{
Logger log = Log.getLogger(LOG_CATEGORY);
log.warn("Property {0} not writable on class {1}",
new Object[] {propertyName, getAlias(instance)});
}
if (!ignorePropertyErrors(context))
{
//Property '{propertyName}' not writable on class '{alias}'.
MessageException ex = new MessageException();
ex.setMessage(NON_WRITABLE_PROPERTY_ERROR, new Object[] {propertyName, getAlias(instance)});
throw ex;
}
}
}
else
{
SerializationContext context = getSerializationContext();
if (Log.isWarn() && logPropertyErrors(context))
{
Logger log = Log.getLogger(LOG_CATEGORY);
log.warn("Ignoring set property {0} for type {1} as a setter could not be found.",
new Object[] {propertyName, getAlias(instance)});
}
if (!ignorePropertyErrors(context))
{
// Property '{propertyName}' not found on class '{alias}'.
MessageException ex = new MessageException();
ex.setMessage(UNKNOWN_PROPERTY_ERROR, new Object[] {propertyName, getAlias(instance)});
throw ex;
}
}
}
/**
* Are we ignoring property errors?
* @param context serialization paramters.
* @return true if ignoring property errors.
*/
protected boolean ignorePropertyErrors(SerializationContext context)
{
return context.ignorePropertyErrors;
}
/**
* Should we log property errors?
* @param context serialization parameters.
* @return true if we should log property errors.
*/
protected boolean logPropertyErrors(SerializationContext context)
{
return context.logPropertyErrors;
}
/**
* Determins the classname for both normal types via Class.getName() and
* virtual types via ASObject.getType(). Virtual types starting
* with the special ">" token are also handled and the underlying
* className is returned.
*
* @param instance the object to examine.
* @return the classname to use for instances of this type
*/
protected String getClassName(Object instance)
{
String className;
if (instance instanceof ASObject)
{
className = ((ASObject)instance).getType();
}
else if (instance instanceof ClassAlias)
{
className = ((ClassAlias)instance).getAlias();
}
else
{
className = instance.getClass().getName();
// If there's an alias, use that as the class name.
ClassAliasRegistry registry = ClassAliasRegistry.getRegistry();
String aliasedClass = registry.getClassName(className);
className = (aliasedClass == null)? className : aliasedClass;
}
return className;
}
/**
* Return a map of properties for a object.
* @param instance object to examine.
* @return a map of Strings to BeanProperty objects.
*/
protected Map getBeanProperties(Object instance)
{
Class c = instance.getClass();
Map props;
if (descriptor == null)
{
props = (Map)beanPropertyCache.get(c);
if (props != null)
{
return props;
}
}
props = new HashMap();
PropertyDescriptor[] pds = getPropertyDescriptors(c);
if (pds == null)
return null;
List excludes = null;
if (descriptor != null)
{
excludes = descriptor.getExcludesForInstance(instance);
if (excludes == null) // For compatibility with older implementations
excludes = descriptor.getExcludes();
}
// Add standard bean properties first
for (int i = 0; i < pds.length; i++)
{
PropertyDescriptor pd = pds[i];
String propertyName = pd.getName();
Method readMethod = pd.getReadMethod();
Method writeMethod = pd.getWriteMethod();
if (readMethod != null && isPublicAccessor(readMethod.getModifiers()))
{
if (!includeReadOnly && writeMethod == null)
continue;
if (excludes != null && excludes.contains(propertyName))
continue;
if (isPropertyIgnored(c, propertyName))
continue;
props.put(propertyName, new BeanProperty(propertyName, pd.getPropertyType(),
readMethod, writeMethod, null));
}
}
// Then add public fields to list if property does not alreay exist
Field[] fields = instance.getClass().getFields();
for (int i = 0; i < fields.length; i++)
{
Field field = fields[i];
String propertyName = field.getName();
int modifiers = field.getModifiers();
if (isPublicField(modifiers) && !props.containsKey(propertyName))
{
if (excludes != null && excludes.contains(propertyName))
continue;
if (isPropertyIgnored(c, propertyName))
continue;
props.put(propertyName,
new BeanProperty(propertyName, field.getType(), null, null, field));
}
}
if (descriptor == null && cacheProperties)
{
synchronized(beanPropertyCache)
{
Map props2 = (Map)beanPropertyCache.get(c);
if (props2 == null)
beanPropertyCache.put(c, props);
else
props = props2;
}
}
return props;
}
/**
* Return a specific property descriptor for a named property.
* @param instance the object to use.
* @param propertyName the property to get.
* @return a descriptor for the property.
*/
protected final BeanProperty getBeanProperty(Object instance, String propertyName)
{
Class c = instance.getClass();
Map props;
// It is faster to use the BeanProperty cache if we are going to cache it.
if (descriptor == null && cacheProperties)
{
props = getBeanProperties(instance);
if (props != null)
return (BeanProperty) props.get(propertyName);
return null;
}
// Otherwise, just build up the property we are asked for
PropertyDescriptorCacheEntry pce = getPropertyDescriptorCacheEntry(c);
if (pce == null)
return null;
Object pType = pce.propertiesByName.get(propertyName);
if (pType == null)
return null;
List excludes = null;
if (descriptor != null)
{
excludes = descriptor.getExcludesForInstance(instance);
if (excludes == null) // For compatibility with older implementations
excludes = descriptor.getExcludes();
}
if (pType instanceof PropertyDescriptor)
{
PropertyDescriptor pd = (PropertyDescriptor) pType;
Method readMethod = pd.getReadMethod();
Method writeMethod = pd.getWriteMethod();
if (readMethod != null && isPublicAccessor(readMethod.getModifiers()))
{
if (!includeReadOnly && writeMethod == null)
return null;
if (excludes != null && excludes.contains(propertyName))
return null;
if (isPropertyIgnored(c, propertyName))
return null;
return new BeanProperty(propertyName, pd.getPropertyType(), readMethod, writeMethod, null);
}
}
else if (pType instanceof Field)
{
Field field = (Field) pType;
String pName = field.getName();
int modifiers = field.getModifiers();
if (isPublicField(modifiers) && pName.equals(propertyName))
{
if (excludes != null && excludes.contains(propertyName))
return null;
if (isPropertyIgnored(c, propertyName))
return null;
return new BeanProperty(propertyName, field.getType(), null, null, field);
}
}
return null;
}
/**
* Return an array of JavaBean property descriptors for a class
* @param c the class to examine.
* @return an array ot JavaBean PropertyDescriptors.
*/
private PropertyDescriptor [] getPropertyDescriptors(Class c)
{
PropertyDescriptorCacheEntry pce = getPropertyDescriptorCacheEntry(c);
if (pce == null)
return null;
return pce.propertyDescriptors;
}
/**
* Return an entry from the property descrpitor cache for a class.
*/
private PropertyDescriptorCacheEntry getPropertyDescriptorCacheEntry(Class c)
{
PropertyDescriptorCacheEntry pce = (PropertyDescriptorCacheEntry) propertyDescriptorCache.get(c);
try
{
if (pce == null)
{
BeanInfo beanInfo = Introspector.getBeanInfo(c, stopClass);
pce = new PropertyDescriptorCacheEntry();
pce.propertyDescriptors = beanInfo.getPropertyDescriptors();
pce.propertiesByName = createPropertiesByNameMap(pce.propertyDescriptors, c.getFields());
if (cachePropertiesDescriptors)
{
synchronized(propertyDescriptorCache)
{
PropertyDescriptorCacheEntry pce2 = (PropertyDescriptorCacheEntry) propertyDescriptorCache.get(c);
if (pce2 == null)
propertyDescriptorCache.put(c, pce);
else
pce = pce2;
}
}
}
}
catch (IntrospectionException ex)
{
// Log failed property set errors
if (Log.isError())
{
Logger log = Log.getLogger(LOG_CATEGORY);
log.error("Failed to introspect object of type: " + c + " error: " + ExceptionUtil.toString(ex));
}
// Return an empty descriptor rather than crashing
pce = new PropertyDescriptorCacheEntry();
pce.propertyDescriptors = new PropertyDescriptor[0];
pce.propertiesByName = new TreeMap();
}
return pce;
}
private Map createPropertiesByNameMap(PropertyDescriptor [] pds, Field [] fields)
{
Map m = new HashMap(pds.length);
for (int i = 0; i < pds.length; i++)
{
PropertyDescriptor pd = pds[i];
Method readMethod = pd.getReadMethod();
if (readMethod != null && isPublicAccessor(readMethod.getModifiers()) &&
(includeReadOnly || pd.getWriteMethod() != null))
m.put(pd.getName(), pd);
}
for (int i = 0; i < fields.length; i++)
{
Field field = fields[i];
if (isPublicField(field.getModifiers()) && !m.containsKey(field.getName()))
m.put(field.getName(), field);
}
return m;
}
/**
* Is this property on the ignore list for this class?
* @param c the class.
* @param propertyName the property name.
* @return true if we should ignore this property.
*/
public static boolean isPropertyIgnored(Class c, String propertyName)
{
boolean result = false;
Set propertyOwners = (Set)ignoreProperties.get(propertyName);
if (propertyOwners != null)
{
while (c != null)
{
if (propertyOwners.contains(c))
{
result = true;
break;
}
c = c.getSuperclass();
}
}
return result;
}
/**
* Add a property to the ignore list for this class.
* @param c the class.
* @param propertyName the property to ignore.
*/
public static void addIgnoreProperty(Class c, String propertyName)
{
synchronized(ignoreProperties)
{
Set propertyOwners = (Set)ignoreProperties.get(propertyName);
if (propertyOwners == null)
{
propertyOwners = new HashSet();
ignoreProperties.put(propertyName, propertyOwners);
}
propertyOwners.add(c);
}
}
/**
* Do the provided modifiers indicate that this is public?
* @param modifiers the flags to check
* @return true if public but not final, static or transient.
*/
public static boolean isPublicField(int modifiers)
{
if (Modifier.isPublic(modifiers) && !Modifier.isFinal(modifiers)
&& !Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers))
{
return true;
}
else
{
return false;
}
}
/**
* Do the provided modifiers indicate that this is public?
* @param modifiers the flags to check
* @return true if public but not static.
*/
public static boolean isPublicAccessor(int modifiers)
{
if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers))
{
return true;
}
else
{
return false;
}
}
/**
* A class that holds information about a bean property.
*/
protected static class BeanProperty
{
private String name;
private Class type;
private Method readMethod, writeMethod;
private Field field;
protected BeanProperty(String name, Class type, Method read, Method write, Field field)
{
this.name = name;
this.type = type;
this.writeMethod = write;
this.readMethod = read;
this.field = field;
}
/**
* The name of the property..
* @return the name.
*/
public String getName()
{
return name;
}
/**
* The type of the property
* @return the type
*/
public Class getType()
{
return type;
}
/**
* Is there a setter for this property?
* @return true if there is a write method.
*/
public boolean isWrite()
{
return writeMethod != null || field != null;
}
/**
* Is there a getter for this property?
* @return true if there is a read method.
*/
public boolean isRead()
{
return readMethod != null || field != null;
}
/**
* Returns the Class object that declared the public field or getter function.
* @return an object of the declaring class for the read method or null if the read method is undefined.
*/
public Class getReadDeclaringClass()
{
if (readMethod != null)
return readMethod.getDeclaringClass();
else if (field != null)
return field.getDeclaringClass();
else
return null;
}
/**
* Return a class that represents the type of the property.
* @return the type of the property or null if there is no read method defined.
*/
public Class getReadType()
{
if (readMethod != null)
return readMethod.getReturnType();
else if (field != null)
return field.getType();
else
return null;
}
/**
*
* Returns a string indicating the setter or field name of the property.
* The setter is prefixed by 'method ', or the field is prefixed by 'field '.
* @return A string suitable for debugging.
*/
public String getWriteName()
{
if (writeMethod != null)
return "method " + writeMethod.getName();
else if (field != null)
return "field " + field.getName();
else
return null;
}
/**
* Set the property of the object to the specified value.
* @param bean the bean to set the property on.
* @param value the value to set.
* @throws IllegalAccessException if no access.
* @throws InvocationTargetException if the setter throws an exception.
*/
public void set(Object bean, Object value) throws IllegalAccessException,
InvocationTargetException
{
if (writeMethod != null)
{
writeMethod.invoke(bean, new Object[] { value });
}
else if (field != null)
{
field.set(bean, value);
}
else
{
throw new MessageException("Setter not found for property " + name);
}
}
/**
* Get the value of this property from the specified object.
* @param bean the object to retrieve the value from
* @return the value of the property.
* @throws IllegalAccessException if no access.
* @throws InvocationTargetException if the getter throws an exception.
*/
public Object get(Object bean) throws IllegalAccessException, InvocationTargetException
{
Object obj = null;
if (readMethod != null)
{
obj = readMethod.invoke(bean, null);
}
else if (field != null)
{
obj = field.get(bean);
}
return obj;
}
}
/** {@inheritDoc} */
public Object clone()
{
return super.clone();
}
/**
* Clears all static caches.
*/
public static void clear()
{
synchronized(ignoreProperties)
{
ignoreProperties.clear();
}
synchronized(propertyNamesCache)
{
propertyNamesCache.clear();
}
synchronized(beanPropertyCache)
{
beanPropertyCache.clear();
}
synchronized(propertyDescriptorCache)
{
propertyDescriptorCache.clear();
}
}
protected static class PropertyDescriptorCacheEntry
{
PropertyDescriptor [] propertyDescriptors;
Map propertiesByName;
}
}