    001    // Copyright 2006, 2007, 2008, 2010 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.ioc.internal.services;
    016
    017    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    018    import org.apache.tapestry5.ioc.services.ClassPropertyAdapter;
    019    import org.apache.tapestry5.ioc.services.PropertyAccess;
    020
    021    import java.beans.BeanInfo;
    022    import java.beans.IntrospectionException;
    023    import java.beans.Introspector;
    024    import java.beans.PropertyDescriptor;
    025    import java.lang.reflect.Method;
    026    import java.lang.reflect.Modifier;
    027    import java.util.Arrays;
    028    import java.util.LinkedList;
    029    import java.util.List;
    030    import java.util.Map;
    031
    032    @SuppressWarnings("unchecked")
    033    public class PropertyAccessImpl implements PropertyAccess
    034    {
    035        private final Map<Class, ClassPropertyAdapter> adapters = CollectionFactory.newConcurrentMap();
    036
    037        public Object get(Object instance, String propertyName)
    038        {
    039            return getAdapter(instance).get(instance, propertyName);
    040        }
    041
    042        public void set(Object instance, String propertyName, Object value)
    043        {
    044            getAdapter(instance).set(instance, propertyName, value);
    045        }
    046
    047        /**
    048         * Clears the cache of adapters and asks the {@link Introspector} to clear its cache.
    049         */
    050        public synchronized void clearCache()
    051        {
    052            adapters.clear();
    053
    054            Introspector.flushCaches();
    055        }
    056
    057        public ClassPropertyAdapter getAdapter(Object instance)
    058        {
    059            return getAdapter(instance.getClass());
    060        }
    061
    062        public ClassPropertyAdapter getAdapter(Class forClass)
    063        {
    064            ClassPropertyAdapter result = adapters.get(forClass);
    065
    066            if (result == null)
    067            {
    068                result = buildAdapter(forClass);
    069                adapters.put(forClass, result);
    070            }
    071
    072            return result;
    073        }
    074
    075        /**
    076         * Builds a new adapter and updates the _adapters cache. This not only guards access to the adapter cache, but also
    077         * serializes access to the Java Beans Introspector, which is not thread safe. In addition, handles the case where
    078         * the class in question is an interface, accumulating properties inherited from super-classes.
    079         */
    080        private synchronized ClassPropertyAdapter buildAdapter(Class forClass)
    081        {
    082            // In some race conditions, we may hit this method for the same class multiple times.
    083            // We just let it happen, replacing the old ClassPropertyAdapter with a new one.
    084
    085            try
    086            {
    087                BeanInfo info = Introspector.getBeanInfo(forClass);
    088
    089                List<PropertyDescriptor> descriptors = CollectionFactory.newList();
    090
    091                addAll(descriptors, info.getPropertyDescriptors());
    092
    093                // TAP5-921 - Introspector misses interface methods not implemented in an abstract class
    094                if (forClass.isInterface() || Modifier.isAbstract(forClass.getModifiers()) )
    095                    addPropertiesFromExtendedInterfaces(forClass, descriptors);
    096
    097                addPropertiesFromScala(forClass, descriptors);
    098
    099                return new ClassPropertyAdapterImpl(forClass, descriptors);
    100            }
    101            catch (Throwable ex)
    102            {
    103                throw new RuntimeException(ex);
    104            }
    105        }
    106
    107        private <T> void addAll(List<T> list, T[] array)
    108        {
    109            list.addAll(Arrays.asList(array));
    110        }
    111
    112        private void addPropertiesFromExtendedInterfaces(Class forClass, List<PropertyDescriptor> descriptors)
    113                throws IntrospectionException
    114        {
    115            LinkedList<Class> queue = CollectionFactory.newLinkedList();
    116
    117            // Seed the queue
    118            addAll(queue, forClass.getInterfaces());
    119
    120            while (!queue.isEmpty())
    121            {
    122                Class c = queue.removeFirst();
    123
    124                BeanInfo info = Introspector.getBeanInfo(c);
    125
    126                // Duplicates occur and are filtered out in ClassPropertyAdapter which stores
    127                // a property name to descriptor map.
    128                addAll(descriptors, info.getPropertyDescriptors());
    129                addAll(queue, c.getInterfaces());
    130            }
    131        }
    132
    133        private void addPropertiesFromScala(Class forClass, List<PropertyDescriptor> descriptors)
    134                throws IntrospectionException
    135        {
    136            for (Method method : forClass.getMethods())
    137            {
    138                addPropertyIfScalaGetterMethod(forClass, descriptors, method);
    139            }
    140        }
    141
    142        private void addPropertyIfScalaGetterMethod(Class forClass, List<PropertyDescriptor> descriptors, Method method)
    143                throws IntrospectionException
    144        {
    145            if (!isScalaGetterMethod(method))
    146                return;
    147
    148            PropertyDescriptor propertyDescriptor = new PropertyDescriptor(method.getName(), forClass, method.getName(),
    149                    null);
    150
    151            // found a getter, looking for the setter now
    152            try
    153            {
    154                Method setterMethod = findScalaSetterMethod(forClass, method);
    155
    156                propertyDescriptor.setWriteMethod(setterMethod);
    157            }
    158            catch (NoSuchMethodException e)
    159            {
    160                // ignore
    161            }
    162
    163            // check if the same property was already discovered with java bean accessors
    164
    165            addScalaPropertyIfNoJavaBeansProperty(descriptors, propertyDescriptor, method);
    166        }
    167
    168        private void addScalaPropertyIfNoJavaBeansProperty(List<PropertyDescriptor> descriptors,
    169                PropertyDescriptor propertyDescriptor, Method getterMethod)
    170        {
    171            boolean found = false;
    172
    173            for (PropertyDescriptor currentPropertyDescriptor : descriptors)
    174            {
    175                if (currentPropertyDescriptor.getName().equals(getterMethod.getName()))
    176                {
    177                    found = true;
    178
    179                    break;
    180                }
    181            }
    182
    183            if (!found)
    184                descriptors.add(propertyDescriptor);
    185        }
    186
    187        private Method findScalaSetterMethod(Class forClass, Method getterMethod) throws NoSuchMethodException
    188        {
    189            return forClass.getMethod(getterMethod.getName() + "_$eq", getterMethod.getReturnType());
    190        }
    191
    192        private boolean isScalaGetterMethod(Method method)
    193        {
    194            try
    195            {
    196                return Modifier.isPublic(method.getModifiers()) && method.getParameterTypes().length == 0
    197                        && !method.getReturnType().equals(Void.TYPE)
    198                        && method.getDeclaringClass().getDeclaredField(method.getName()) != null;
    199            }
    200            catch (NoSuchFieldException ex)
    201            {
    202                return false;
    203            }
    204        }
    205    }