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