    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.beanmodel.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.commons.services.ClassPropertyAdapter;
    030import org.apache.tapestry5.commons.services.PropertyAccess;
    031import org.apache.tapestry5.commons.util.CollectionFactory;
    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            // Introspector misses:
    106            // - interface methods not implemented in an abstract class (TAP5-921)
    107            // - default methods (TAP5-2449)
    108            addPropertiesFromExtendedInterfaces(forClass, descriptors);
    109
    110            addPropertiesFromScala(forClass, descriptors);
    111
    112
    113            return new ClassPropertyAdapterImpl(forClass, descriptors);
    114        }
    115        catch (Throwable ex)
    116        {
    117            throw new RuntimeException(ex);
    118        }
    119    }
    120
    121    private static <T> void addAll(List<T> list, T[] array)
    122    {
    123        if (array.length > 0){
    124            list.addAll(Arrays.asList(array));
    125        }
    126    }
    127
    128    private static <T> void addInterfacesRecursively(List<Class> list, Class<?> clazz)
    129    {
    130        Class<?>[] interfaces = clazz.getInterfaces();
    131        for (int i = 0; i < interfaces.length; i++) {
    132          Class<?> iface = interfaces[i];
    133          addInterfacesRecursively(list, iface);
    134          list.add(iface);
    135        }
    136    }
    137
    138    private static void addPropertiesFromExtendedInterfaces(Class forClass, List<PropertyDescriptor> descriptors)
    139            throws IntrospectionException
    140    {
    141
    142        Class[] interfaces = forClass.getInterfaces();
    143        if (interfaces.length == 0){
    144            return;
    145        }
    146        LinkedList<Class> queue = CollectionFactory.newLinkedList();
    147        // Seed the queue
    148        addInterfacesRecursively(queue, forClass);
    149
    150        while (!queue.isEmpty())
    151        {
    152            Class c = queue.removeFirst();
    153
    154            BeanInfo info = Introspector.getBeanInfo(c);
    155
    156            // Duplicates occur and are filtered out in ClassPropertyAdapter which stores
    157            // a property name to descriptor map.
    158            addAll(descriptors, info.getPropertyDescriptors());
    159        }
    160    }
    161
    162    private void addPropertiesFromScala(Class forClass, List<PropertyDescriptor> descriptors)
    163            throws IntrospectionException
    164    {
    165        for (Method method : forClass.getMethods())
    166        {
    167            addPropertyIfScalaGetterMethod(forClass, descriptors, method);
    168        }
    169    }
    170
    171    private void addPropertyIfScalaGetterMethod(Class forClass, List<PropertyDescriptor> descriptors, Method method)
    172            throws IntrospectionException
    173    {
    174        if (!isScalaGetterMethod(method))
    175            return;
    176
    177        PropertyDescriptor propertyDescriptor = new PropertyDescriptor(method.getName(), forClass, method.getName(),
    178                null);
    179
    180        // found a getter, looking for the setter now
    181        try
    182        {
    183            Method setterMethod = findScalaSetterMethod(forClass, method);
    184
    185            propertyDescriptor.setWriteMethod(setterMethod);
    186        }
    187        catch (NoSuchMethodException e)
    188        {
    189            // ignore
    190        }
    191
    192        // check if the same property was already discovered with java bean accessors
    193
    194        addScalaPropertyIfNoJavaBeansProperty(descriptors, propertyDescriptor, method);
    195    }
    196
    197    private void addScalaPropertyIfNoJavaBeansProperty(List<PropertyDescriptor> descriptors,
    198            PropertyDescriptor propertyDescriptor, Method getterMethod)
    199    {
    200        boolean found = false;
    201
    202        for (PropertyDescriptor currentPropertyDescriptor : descriptors)
    203        {
    204            if (currentPropertyDescriptor.getName().equals(getterMethod.getName()))
    205            {
    206                found = true;
    207
    208                break;
    209            }
    210        }
    211
    212        if (!found)
    213            descriptors.add(propertyDescriptor);
    214    }
    215
    216    private Method findScalaSetterMethod(Class forClass, Method getterMethod) throws NoSuchMethodException
    217    {
    218        return forClass.getMethod(getterMethod.getName() + "_$eq", getterMethod.getReturnType());
    219    }
    220
    221    private boolean isScalaGetterMethod(Method method)
    222    {
    223        try
    224        {
    225            return Modifier.isPublic(method.getModifiers()) && method.getParameterTypes().length == 0
    226                    && !method.getReturnType().equals(Void.TYPE)
    227                    && method.getDeclaringClass().getDeclaredField(method.getName()) != null;
    228        }
    229        catch (NoSuchFieldException ex)
    230        {
    231            return false;
    232        }
    233    }
    234}