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