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    @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            // TAP5-921 - Introspector misses interface methods not implemented in an abstract class
106            if (forClass.isInterface() || Modifier.isAbstract(forClass.getModifiers()) )
107                addPropertiesFromExtendedInterfaces(forClass, descriptors);
108
109            addPropertiesFromScala(forClass, descriptors);
110
111            return new ClassPropertyAdapterImpl(forClass, descriptors);
112        }
113        catch (Throwable ex)
114        {
115            throw new RuntimeException(ex);
116        }
117    }
118
119    private <T> void addAll(List<T> list, T[] array)
120    {
121        list.addAll(Arrays.asList(array));
122    }
123
124    private void addPropertiesFromExtendedInterfaces(Class forClass, List<PropertyDescriptor> descriptors)
125            throws IntrospectionException
126    {
127        LinkedList<Class> queue = CollectionFactory.newLinkedList();
128
129        // Seed the queue
130        addAll(queue, forClass.getInterfaces());
131
132        while (!queue.isEmpty())
133        {
134            Class c = queue.removeFirst();
135
136            BeanInfo info = Introspector.getBeanInfo(c);
137
138            // Duplicates occur and are filtered out in ClassPropertyAdapter which stores
139            // a property name to descriptor map.
140            addAll(descriptors, info.getPropertyDescriptors());
141            addAll(queue, c.getInterfaces());
142        }
143    }
144
145    private void addPropertiesFromScala(Class forClass, List<PropertyDescriptor> descriptors)
146            throws IntrospectionException
147    {
148        for (Method method : forClass.getMethods())
149        {
150            addPropertyIfScalaGetterMethod(forClass, descriptors, method);
151        }
152    }
153
154    private void addPropertyIfScalaGetterMethod(Class forClass, List<PropertyDescriptor> descriptors, Method method)
155            throws IntrospectionException
156    {
157        if (!isScalaGetterMethod(method))
158            return;
159
160        PropertyDescriptor propertyDescriptor = new PropertyDescriptor(method.getName(), forClass, method.getName(),
161                null);
162
163        // found a getter, looking for the setter now
164        try
165        {
166            Method setterMethod = findScalaSetterMethod(forClass, method);
167
168            propertyDescriptor.setWriteMethod(setterMethod);
169        }
170        catch (NoSuchMethodException e)
171        {
172            // ignore
173        }
174
175        // check if the same property was already discovered with java bean accessors
176
177        addScalaPropertyIfNoJavaBeansProperty(descriptors, propertyDescriptor, method);
178    }
179
180    private void addScalaPropertyIfNoJavaBeansProperty(List<PropertyDescriptor> descriptors,
181            PropertyDescriptor propertyDescriptor, Method getterMethod)
182    {
183        boolean found = false;
184
185        for (PropertyDescriptor currentPropertyDescriptor : descriptors)
186        {
187            if (currentPropertyDescriptor.getName().equals(getterMethod.getName()))
188            {
189                found = true;
190
191                break;
192            }
193        }
194
195        if (!found)
196            descriptors.add(propertyDescriptor);
197    }
198
199    private Method findScalaSetterMethod(Class forClass, Method getterMethod) throws NoSuchMethodException
200    {
201        return forClass.getMethod(getterMethod.getName() + "_$eq", getterMethod.getReturnType());
202    }
203
204    private boolean isScalaGetterMethod(Method method)
205    {
206        try
207        {
208            return Modifier.isPublic(method.getModifiers()) && method.getParameterTypes().length == 0
209                    && !method.getReturnType().equals(Void.TYPE)
210                    && method.getDeclaringClass().getDeclaredField(method.getName()) != null;
211        }
212        catch (NoSuchFieldException ex)
213        {
214            return false;
215        }
216    }
217}