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