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