001// Copyright 2006, 2007, 2008, 2010 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 java.beans.BeanInfo;
018import java.beans.IntrospectionException;
019import java.beans.Introspector;
020import java.beans.PropertyDescriptor;
021import java.lang.annotation.Annotation;
022import java.lang.reflect.Method;
023import java.lang.reflect.Modifier;
024import java.util.Arrays;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028
029import org.apache.tapestry5.commons.services.ClassPropertyAdapter;
030import org.apache.tapestry5.commons.services.PropertyAccess;
031import org.apache.tapestry5.commons.util.CollectionFactory;
032
033@SuppressWarnings("unchecked")
034public class PropertyAccessImpl implements PropertyAccess
035{
036    private final Map<Class, ClassPropertyAdapter> adapters = CollectionFactory.newConcurrentMap();
037
038    @Override
039    public Object get(Object instance, String propertyName)
040    {
041        return getAdapter(instance).get(instance, propertyName);
042    }
043
044    @Override
045    public void set(Object instance, String propertyName, Object value)
046    {
047        getAdapter(instance).set(instance, propertyName, value);
048    }
049
050    @Override
051    public Annotation getAnnotation(Object instance, String propertyName, Class<? extends Annotation> annotationClass) {
052    return getAdapter(instance).getAnnotation(instance, propertyName, annotationClass);
053    }
054
055
056    /**
057     * Clears the cache of adapters and asks the {@link Introspector} to clear its cache.
058     */
059    @Override
060    public synchronized void clearCache()
061    {
062        adapters.clear();
063
064        Introspector.flushCaches();
065    }
066
067    @Override
068    public ClassPropertyAdapter getAdapter(Object instance)
069    {
070        return getAdapter(instance.getClass());
071    }
072
073    @Override
074    public ClassPropertyAdapter getAdapter(Class forClass)
075    {
076        ClassPropertyAdapter result = adapters.get(forClass);
077
078        if (result == null)
079        {
080            result = buildAdapter(forClass);
081            adapters.put(forClass, result);
082        }
083
084        return result;
085    }
086
087    /**
088     * Builds a new adapter and updates the _adapters cache. This not only guards access to the adapter cache, but also
089     * serializes access to the Java Beans Introspector, which is not thread safe. In addition, handles the case where
090     * the class in question is an interface, accumulating properties inherited from super-classes.
091     */
092    private synchronized ClassPropertyAdapter buildAdapter(Class forClass)
093    {
094        // In some race conditions, we may hit this method for the same class multiple times.
095        // We just let it happen, replacing the old ClassPropertyAdapter with a new one.
096
097        try
098        {
099            BeanInfo info = Introspector.getBeanInfo(forClass);
100
101            List<PropertyDescriptor> descriptors = CollectionFactory.newList();
102
103            addAll(descriptors, info.getPropertyDescriptors());
104
105            // Introspector misses:
106            // - interface methods not implemented in an abstract class (TAP5-921)
107            // - default methods (TAP5-2449)
108            addPropertiesFromExtendedInterfaces(forClass, descriptors);
109
110            addPropertiesFromScala(forClass, descriptors);
111
112
113            return new ClassPropertyAdapterImpl(forClass, descriptors);
114        }
115        catch (Throwable ex)
116        {
117            throw new RuntimeException(ex);
118        }
119    }
120
121    private static <T> void addAll(List<T> list, T[] array)
122    {
123        if (array.length > 0){
124            list.addAll(Arrays.asList(array));
125        }
126    }
127
128    private static <T> void addInterfacesRecursively(List<Class> list, Class<?> clazz)
129    {
130        Class<?>[] interfaces = clazz.getInterfaces();
131        for (int i = 0; i < interfaces.length; i++) {
132          Class<?> iface = interfaces[i];
133          addInterfacesRecursively(list, iface);
134          list.add(iface);
135        }
136    }
137
138    private static void addPropertiesFromExtendedInterfaces(Class forClass, List<PropertyDescriptor> descriptors)
139            throws IntrospectionException
140    {
141
142        Class[] interfaces = forClass.getInterfaces();
143        if (interfaces.length == 0){
144            return;
145        }
146        LinkedList<Class> queue = CollectionFactory.newLinkedList();
147        // Seed the queue
148        addInterfacesRecursively(queue, forClass);
149
150        while (!queue.isEmpty())
151        {
152            Class c = queue.removeFirst();
153
154            BeanInfo info = Introspector.getBeanInfo(c);
155
156            // Duplicates occur and are filtered out in ClassPropertyAdapter which stores
157            // a property name to descriptor map.
158            addAll(descriptors, info.getPropertyDescriptors());
159        }
160    }
161
162    private void addPropertiesFromScala(Class forClass, List<PropertyDescriptor> descriptors)
163            throws IntrospectionException
164    {
165        for (Method method : forClass.getMethods())
166        {
167            addPropertyIfScalaGetterMethod(forClass, descriptors, method);
168        }
169    }
170
171    private void addPropertyIfScalaGetterMethod(Class forClass, List<PropertyDescriptor> descriptors, Method method)
172            throws IntrospectionException
173    {
174        if (!isScalaGetterMethod(method))
175            return;
176
177        PropertyDescriptor propertyDescriptor = new PropertyDescriptor(method.getName(), forClass, method.getName(),
178                null);
179
180        // found a getter, looking for the setter now
181        try
182        {
183            Method setterMethod = findScalaSetterMethod(forClass, method);
184
185            propertyDescriptor.setWriteMethod(setterMethod);
186        }
187        catch (NoSuchMethodException e)
188        {
189            // ignore
190        }
191
192        // check if the same property was already discovered with java bean accessors
193
194        addScalaPropertyIfNoJavaBeansProperty(descriptors, propertyDescriptor, method);
195    }
196
197    private void addScalaPropertyIfNoJavaBeansProperty(List<PropertyDescriptor> descriptors,
198            PropertyDescriptor propertyDescriptor, Method getterMethod)
199    {
200        boolean found = false;
201
202        for (PropertyDescriptor currentPropertyDescriptor : descriptors)
203        {
204            if (currentPropertyDescriptor.getName().equals(getterMethod.getName()))
205            {
206                found = true;
207
208                break;
209            }
210        }
211
212        if (!found)
213            descriptors.add(propertyDescriptor);
214    }
215
216    private Method findScalaSetterMethod(Class forClass, Method getterMethod) throws NoSuchMethodException
217    {
218        return forClass.getMethod(getterMethod.getName() + "_$eq", getterMethod.getReturnType());
219    }
220
221    private boolean isScalaGetterMethod(Method method)
222    {
223        try
224        {
225            return Modifier.isPublic(method.getModifiers()) && method.getParameterTypes().length == 0
226                    && !method.getReturnType().equals(Void.TYPE)
227                    && method.getDeclaringClass().getDeclaredField(method.getName()) != null;
228        }
229        catch (NoSuchFieldException ex)
230        {
231            return false;
232        }
233    }
234}