blob: 05ccb01d67f0866699d4d125dd012caf764abbae [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package flex.messaging.io;
import flex.messaging.MessageException;
import flex.messaging.io.amf.ASObject;
import flex.messaging.log.Log;
import flex.messaging.log.Logger;
import flex.messaging.util.ClassUtil;
import flex.messaging.util.ExceptionUtil;
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.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Uses Bean introspection to collect the properties for a given instance.
*/
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<Class, List<String>> rwPropertyNamesCache = new IdentityHashMap<Class, List<String>>();
protected static final Map<Class, Map<String, BeanProperty>> rwBeanPropertyCache = new IdentityHashMap<Class, Map<String, BeanProperty>>();
protected static final Map<Class, PropertyDescriptorCacheEntry> rwPropertyDescriptorCache = new IdentityHashMap<Class, PropertyDescriptorCacheEntry>();
protected static final Map<Class, List<String>> roPropertyNamesCache = new IdentityHashMap<Class, List<String>>();
protected static final Map<Class, Map<String, BeanProperty>> roBeanPropertyCache = new IdentityHashMap<Class, Map<String, BeanProperty>>();
protected static final Map<Class, PropertyDescriptorCacheEntry> roPropertyDescriptorCache = new IdentityHashMap<Class, PropertyDescriptorCacheEntry>();
protected boolean cacheProperties = true;
protected boolean cachePropertiesDescriptors = true;
protected Class stopClass = Object.class;
protected static final Map ignoreProperties = new HashMap();
static
{
initializeIgnoreProperties();
}
private static void initializeIgnoreProperties()
{
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<String> propertyNames = null;
// Look up property names in cache if we don't have a custom serialization descriptor
if (descriptor == null)
{
if (getIncludeReadOnly())
{
synchronized (roPropertyNamesCache)
{
propertyNames = roPropertyNamesCache.get(c);
}
}
else
{
synchronized (rwPropertyNamesCache)
{
propertyNames = rwPropertyNamesCache.get(c);
}
}
}
if (propertyNames != null)
return propertyNames;
// Make a copy of the property names to return
propertyNames = new ArrayList<String>(getBeanProperties(instance).keySet());
// Store property names in cache if we are caching properties
// and we don't have a custom serialization descriptor
if (cacheProperties && descriptor == null)
{
if (getIncludeReadOnly())
{
synchronized (roPropertyNamesCache)
{
roPropertyNamesCache.put(c, propertyNames);
}
}
else
{
synchronized (rwPropertyNamesCache)
{
rwPropertyNamesCache.put(c, propertyNames);
}
}
}
return propertyNames;
}
/** {@inheritDoc} */
public Class getType(Object instance, String propertyName)
{
if (instance == null || propertyName == null)
return null;
BeanProperty bp = getBeanProperty(instance, propertyName);
return bp == null? null : bp.getType();
}
/** {@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);
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);
ClassUtil.validateAssignment(instance, propertyName, value);
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<String, BeanProperty> getBeanProperties(Object instance)
{
Class c = instance.getClass();
Map<String, BeanProperty> props;
// look up instance class in cache if we don't have a custom descriptor.
if (descriptor == null)
{
if (getIncludeReadOnly())
{
synchronized (roBeanPropertyCache)
{
props = roBeanPropertyCache.get(c);
}
}
else
{
synchronized (rwBeanPropertyCache)
{
props = rwBeanPropertyCache.get(c);
}
}
if (props != null)
return props;
}
props = new HashMap<String, BeanProperty>();
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 (PropertyDescriptor pd : pds)
{
String propertyName = pd.getName();
Method readMethod = pd.getReadMethod();
Method writeMethod = pd.getWriteMethod();
// If there's a public read method but no writeMethod and includeReadOnly
// flag is off, then skip the property.
if (readMethod != null && isPublicAccessor(readMethod.getModifiers()) && !getIncludeReadOnly() && writeMethod == null)
continue;
// Skip excluded and ignored properties as well.
if ((excludes != null && excludes.contains(propertyName)) || isPropertyIgnored(c, propertyName))
continue;
// Ensure we don't include Object getClass() property, possibly returned (incorrectly) by custom BeanInfos
if (getIncludeReadOnly() && writeMethod == null && "class".equals(propertyName))
continue;
// Skip any classloader properties
final Class<?> type = pd.getPropertyType();
if (type != null && ClassLoader.class.isAssignableFrom(type))
continue;
props.put(propertyName, new BeanProperty(propertyName, pd.getPropertyType(),
readMethod, writeMethod, null));
}
// Then add public fields to list if property does not already exist
Field[] fields = instance.getClass().getFields();
for (Field field : fields)
{
String propertyName = field.getName();
int modifiers = field.getModifiers();
if (isPublicField(modifiers) && !props.containsKey(propertyName))
{
// Skip excluded and ignored properties.
if ((excludes != null && excludes.contains(propertyName)) || isPropertyIgnored(c, propertyName))
continue;
props.put(propertyName, new BeanProperty(propertyName, field.getType(), null, null, field));
}
}
// Update the cache if we don't have a custom serialization descriptor and we are caching.
if (descriptor == null && cacheProperties)
{
if (getIncludeReadOnly())
{
synchronized (roBeanPropertyCache)
{
roBeanPropertyCache.put(c, props);
}
}
else
{
synchronized (rwBeanPropertyCache)
{
rwBeanPropertyCache.put(c, props);
}
}
}
return props;
}
/**
* Return true if this property is write only, which means we cannot get a value for it.
*
* @param instance the instance
* @param propertyName the property name
* @return true if there is a way to write but not read the property
*/
public boolean isWriteOnly(Object instance, String propertyName)
{
if (instance == null || propertyName == null)
return false;
BeanProperty bp = getBeanProperty(instance, propertyName);
return bp != null && bp.isWrite() && !bp.isRead();
}
/**
* 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);
return props == null? null : (BeanProperty)props.get(propertyName);
}
// 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 there's a public read method but no writeMethod and includeReadOnly
// flag is off, then skip the property.
if (readMethod != null && isPublicAccessor(readMethod.getModifiers()) && !getIncludeReadOnly() && writeMethod == null)
return null;
// Skip excluded and ignored properties as well.
if ((excludes != null && excludes.contains(propertyName)) || 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))
{
// Skip excluded and ignored properties.
return ((excludes != null && excludes.contains(propertyName)) || isPropertyIgnored(c, propertyName))?
null : 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);
return pce == null? null : pce.propertyDescriptors;
}
/**
* Return an entry from the property descriptor cache for a class.
* @param c the class
* @return a descriptor cache entry or null
*/
private PropertyDescriptorCacheEntry getPropertyDescriptorCacheEntry(Class c)
{
PropertyDescriptorCacheEntry pce;
if (getIncludeReadOnly())
{
synchronized (roPropertyDescriptorCache)
{
pce = roPropertyDescriptorCache.get(c);
}
}
else
{
synchronized (rwPropertyDescriptorCache)
{
pce = rwPropertyDescriptorCache.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)
{
if (getIncludeReadOnly())
{
synchronized (roPropertyDescriptorCache)
{
roPropertyDescriptorCache.put(c, pce);
}
}
else
{
synchronized (rwPropertyDescriptorCache)
{
rwPropertyDescriptorCache.put(c, pce);
}
}
}
}
}
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 (PropertyDescriptor pd : pds)
{
Method readMethod = pd.getReadMethod();
if (readMethod != null && isPublicAccessor(readMethod.getModifiers()) &&
(getIncludeReadOnly() || pd.getWriteMethod() != null))
m.put(pd.getName(), pd);
}
for (Field field : fields)
{
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)
{
return (Modifier.isPublic(modifiers) && !Modifier.isFinal(modifiers)
&& !Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers));
}
/**
* 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)
{
return (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers));
}
/**
* 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();
if (field != null)
return field.getDeclaringClass();
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();
if (field != null)
return field.getType();
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();
if (field != null)
return "field " + field.getName();
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, 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, (Object[])null);
else if (field != null)
obj = field.get(bean);
return obj;
}
}
/**
* Clears all static caches.
*/
public static void clear()
{
synchronized(ignoreProperties)
{
ignoreProperties.clear();
initializeIgnoreProperties(); // reset to original state
}
synchronized(rwPropertyNamesCache)
{
rwPropertyNamesCache.clear();
}
synchronized(rwBeanPropertyCache)
{
rwBeanPropertyCache.clear();
}
synchronized(rwPropertyDescriptorCache)
{
rwPropertyDescriptorCache.clear();
}
synchronized(roPropertyNamesCache)
{
roPropertyNamesCache.clear();
}
synchronized(roBeanPropertyCache)
{
roBeanPropertyCache.clear();
}
synchronized(roPropertyDescriptorCache)
{
roPropertyDescriptorCache.clear();
}
}
/**
* A cache entry.
*/
protected static class PropertyDescriptorCacheEntry
{
PropertyDescriptor [] propertyDescriptors;
Map propertiesByName;
}
}