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    
015    package org.apache.tapestry5.ioc.internal.services;
016    
017    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
018    import org.apache.tapestry5.ioc.services.ClassPropertyAdapter;
019    import org.apache.tapestry5.ioc.services.PropertyAccess;
020    
021    import java.beans.BeanInfo;
022    import java.beans.IntrospectionException;
023    import java.beans.Introspector;
024    import java.beans.PropertyDescriptor;
025    import java.lang.reflect.Method;
026    import java.lang.reflect.Modifier;
027    import java.util.Arrays;
028    import java.util.LinkedList;
029    import java.util.List;
030    import java.util.Map;
031    
032    @SuppressWarnings("unchecked")
033    public 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    }