/*
 * 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;
    }
}
