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