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