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}