    001// Copyright 2006, 2007, 2008, 2010, 2011, 2012 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 static org.apache.tapestry5.commons.util.CollectionFactory.newCaseInsensitiveMap;
    018
    019import java.beans.PropertyDescriptor;
    020import java.lang.annotation.Annotation;
    021import java.lang.reflect.Field;
    022import java.lang.reflect.Method;
    023import java.util.List;
    024import java.util.Map;
    025
    026import org.apache.tapestry5.commons.internal.services.ServiceMessages;
    027import org.apache.tapestry5.commons.internal.util.GenericsUtils;
    028import org.apache.tapestry5.commons.internal.util.InternalCommonsUtils;
    029import org.apache.tapestry5.commons.services.ClassPropertyAdapter;
    030import org.apache.tapestry5.commons.services.PropertyAdapter;
    031import org.apache.tapestry5.commons.util.CollectionFactory;
    032
    033public class ClassPropertyAdapterImpl implements ClassPropertyAdapter
    034{
    035    private final Map<String, PropertyAdapter> adapters = newCaseInsensitiveMap();
    036
    037    private final Class beanType;
    038
    039    public ClassPropertyAdapterImpl(Class beanType, List<PropertyDescriptor> descriptors)
    040    {
    041        this.beanType = beanType;
    042
    043        // lazy init
    044        Map<String, List<Method>> nonBridgeMethods = null;
    045
    046        for (PropertyDescriptor pd : descriptors)
    047        {
    048            // Indexed properties will have a null propertyType (and a non-null
    049            // indexedPropertyType). We ignore indexed properties.
    050
    051            String name = pd.getName();
    052
    053            if (adapters.containsKey(name))
    054            {
    055                continue;
    056            }
    057
    058            final Class<?> thisPropertyType = pd.getPropertyType();
    059            if (thisPropertyType == null)
    060                continue;
    061
    062            Method readMethod = pd.getReadMethod();
    063            Method writeMethod = pd.getWriteMethod();
    064
    065            // TAP5-1493
    066            if (readMethod != null && readMethod.isBridge())
    067            {
    068                if (nonBridgeMethods == null)
    069                {
    070                    nonBridgeMethods = groupNonBridgeMethodsByName(beanType);
    071                }
    072                readMethod = findMethodWithSameNameAndParamCount(readMethod, nonBridgeMethods);
    073            }
    074
    075            // TAP5-1548, TAP5-1885: trying to find a getter which Introspector missed
    076            if (readMethod == null) {
    077                final String prefix = thisPropertyType != boolean.class ? "get" : "is";
    078                try
    079                {
    080                    Method method = beanType.getMethod(prefix + capitalize(name));
    081                    final Class<?> returnType = method.getReturnType();
    082                    if (returnType.equals(thisPropertyType) || returnType.isInstance(thisPropertyType)) {
    083                        readMethod = method;
    084                    }
    085                }
    086                catch (SecurityException e) {
    087                    // getter not usable.
    088                }
    089                catch (NoSuchMethodException e)
    090                {
    091                    // getter doesn't exist.
    092                }
    093            }
    094
    095            if (writeMethod != null && writeMethod.isBridge())
    096            {
    097                if (nonBridgeMethods == null)
    098                {
    099                    nonBridgeMethods = groupNonBridgeMethodsByName(beanType);
    100                }
    101                writeMethod = findMethodWithSameNameAndParamCount(writeMethod, nonBridgeMethods);
    102            }
    103
    104            // TAP5-1548, TAP5-1885: trying to find a setter which Introspector missed
    105            if (writeMethod == null) {
    106                try
    107                {
    108                    Method method = beanType.getMethod("set" + capitalize(name), pd.getPropertyType());
    109                    final Class<?> returnType = method.getReturnType();
    110                    if (returnType.equals(void.class)) {
    111                        writeMethod = method;
    112                    }
    113                }
    114                catch (SecurityException e) {
    115                    // setter not usable.
    116                }
    117                catch (NoSuchMethodException e)
    118                {
    119                    // setter doesn't exist.
    120                }
    121            }
    122
    123            Class propertyType = readMethod == null ? thisPropertyType : GenericsUtils.extractGenericReturnType(
    124                    beanType, readMethod);
    125
    126            PropertyAdapter pa = new PropertyAdapterImpl(this, name, propertyType, readMethod, writeMethod);
    127
    128            adapters.put(pa.getName(), pa);
    129        }
    130
    131        // Now, add any public fields (even if static) that do not conflict
    132
    133        for (Field f : beanType.getFields())
    134        {
    135            String name = f.getName();
    136
    137            if (!adapters.containsKey(name))
    138            {
    139                Class propertyType = GenericsUtils.extractGenericFieldType(beanType, f);
    140                PropertyAdapter pa = new PropertyAdapterImpl(this, name, propertyType, f);
    141
    142                adapters.put(name, pa);
    143            }
    144        }
    145    }
    146
    147    private static String capitalize(String name)
    148    {
    149        return Character.toUpperCase(name.charAt(0)) + name.substring(1);
    150    }
    151
    152    /**
    153     * Find a replacement for the method (if one exists)
    154     * @param method A method
    155     * @param groupedMethods Methods mapped by name
    156     * @return A method from groupedMethods with the same name / param count
    157     *         (default to providedmethod if none found)
    158     */
    159    private Method findMethodWithSameNameAndParamCount(Method method, Map<String, List<Method>> groupedMethods) {
    160        List<Method> methodGroup = groupedMethods.get(method.getName());
    161        if (methodGroup != null)
    162        {
    163            for (Method nonBridgeMethod : methodGroup)
    164            {
    165                if (nonBridgeMethod.getParameterTypes().length == method.getParameterTypes().length)
    166                {
    167                    // return the non-bridge method with the same name / argument count
    168                    return nonBridgeMethod;
    169                }
    170            }
    171        }
    172
    173        // default to the provided method
    174        return method;
    175    }
    176
    177    /**
    178     * Find all of the public methods that are not bridge methods and
    179     * group them by method name
    180     *
    181     * {@see Method#isBridge()}
    182     * @param type Bean type
    183     * @return
    184     */
    185    private Map<String, List<Method>> groupNonBridgeMethodsByName(Class type)
    186    {
    187        Map<String, List<Method>> methodGroupsByName = CollectionFactory.newMap();
    188        for (Method method : type.getMethods())
    189        {
    190            if (!method.isBridge())
    191            {
    192                List<Method> methodGroup = methodGroupsByName.get(method.getName());
    193                if (methodGroup == null)
    194                {
    195                    methodGroup = CollectionFactory.newList();
    196                    methodGroupsByName.put(method.getName(), methodGroup);
    197                }
    198                methodGroup.add(method);
    199            }
    200        }
    201        return methodGroupsByName;
    202    }
    203
    204    @Override
    205    public Class getBeanType()
    206    {
    207        return beanType;
    208    }
    209
    210    @Override
    211    public String toString()
    212    {
    213        String names = InternalCommonsUtils.joinSorted(adapters.keySet());
    214
    215        return String.format("<ClassPropertyAdaptor %s: %s>", beanType.getName(), names);
    216    }
    217
    218    @Override
    219    public List<String> getPropertyNames()
    220    {
    221        return InternalCommonsUtils.sortedKeys(adapters);
    222    }
    223
    224    @Override
    225    public PropertyAdapter getPropertyAdapter(String name)
    226    {
    227        return adapters.get(name);
    228    }
    229
    230    @Override
    231    public Object get(Object instance, String propertyName)
    232    {
    233        return adaptorFor(propertyName).get(instance);
    234    }
    235
    236    @Override
    237    public void set(Object instance, String propertyName, Object value)
    238    {
    239        adaptorFor(propertyName).set(instance, value);
    240    }
    241
    242    @Override
    243    public Annotation getAnnotation(Object instance, String propertyName, Class<? extends Annotation> annotationClass) {
    244    return adaptorFor(propertyName).getAnnotation(annotationClass);
    245    }
    246
    247    private PropertyAdapter adaptorFor(String name)
    248    {
    249        PropertyAdapter pa = adapters.get(name);
    250
    251        if (pa == null)
    252            throw new IllegalArgumentException(ServiceMessages.noSuchProperty(beanType, name));
    253
    254        return pa;
    255    }
    256
    257}