001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one or more
003     *  contributor license agreements.  See the NOTICE file distributed with
004     *  this work for additional information regarding copyright ownership.
005     *  The ASF licenses this file to You under the Apache License, Version 2.0
006     *  (the "License"); you may not use this file except in compliance with
007     *  the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package org.granite.util;
019    
020    import java.lang.reflect.Method;
021    import java.lang.reflect.Modifier;
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.HashMap;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Map.Entry;
028    import java.util.WeakHashMap;
029    
030    /**
031     *      Basic bean introspector
032     *  Required for Android environment which does not include java.beans.Intropector
033     */
034    public class Introspector {
035    
036        private static Map<Class<?>, PropertyDescriptor[]> descriptorCache = Collections.synchronizedMap(new WeakHashMap<Class<?>, PropertyDescriptor[]>(128));
037    
038        /**
039         * Decapitalizes a given string according to the rule:
040         * <ul>
041         * <li>If the first or only character is Upper Case, it is made Lower Case
042         * <li>UNLESS the second character is also Upper Case, when the String is
043         * returned unchanged <eul>
044         * 
045         * @param name -
046         *            the String to decapitalize
047         * @return the decapitalized version of the String
048         */
049        public static String decapitalize(String name) {
050    
051            if (name == null)
052                return null;
053            // The rule for decapitalize is that:
054            // If the first letter of the string is Upper Case, make it lower case
055            // UNLESS the second letter of the string is also Upper Case, in which case no
056            // changes are made.
057            if (name.length() == 0 || (name.length() > 1 && Character.isUpperCase(name.charAt(1)))) {
058                return name;
059            }
060            
061            char[] chars = name.toCharArray();
062            chars[0] = Character.toLowerCase(chars[0]);
063            return new String(chars);
064        }
065    
066        /**
067         * Flushes all <code>BeanInfo</code> caches.
068         *  
069         */
070        public static void flushCaches() {
071            // Flush the cache by throwing away the cache HashMap and creating a
072            // new empty one
073            descriptorCache.clear();
074        }
075    
076        /**
077         * Flushes the <code>BeanInfo</code> caches of the specified bean class
078         * 
079         * @param clazz
080         *            the specified bean class
081         */
082        public static void flushFromCaches(Class<?> clazz) {
083            if (clazz == null)
084                throw new NullPointerException();
085            
086            descriptorCache.remove(clazz);
087        }
088    
089        /**
090             * Gets the <code>BeanInfo</code> object which contains the information of
091             * the properties, events and methods of the specified bean class.
092             * 
093             * <p>
094             * The <code>Introspector</code> will cache the <code>BeanInfo</code>
095             * object. Subsequent calls to this method will be answered with the cached
096             * data.
097             * </p>
098             * 
099             * @param beanClass
100             *            the specified bean class.
101             * @return the <code>BeanInfo</code> of the bean class.
102             * @throws IntrospectionException
103             */
104        public static PropertyDescriptor[] getPropertyDescriptors(Class<?> beanClass) {
105            PropertyDescriptor[] descriptor = descriptorCache.get(beanClass);
106            if (descriptor == null) {
107                    descriptor = new BeanInfo(beanClass).getPropertyDescriptors();
108                descriptorCache.put(beanClass, descriptor);
109            }
110            return descriptor;
111        }
112        
113        
114        private static class BeanInfo {
115    
116            private Class<?> beanClass;
117            private PropertyDescriptor[] properties = null;
118    
119            
120            public BeanInfo(Class<?> beanClass) {
121                this.beanClass = beanClass;
122    
123                if (properties == null)
124                    properties = introspectProperties();
125            }
126    
127            public PropertyDescriptor[] getPropertyDescriptors() {
128                return properties;
129            }
130    
131            /**
132             * Introspects the supplied class and returns a list of the Properties of
133             * the class
134             * 
135             * @return The list of Properties as an array of PropertyDescriptors
136             * @throws IntrospectionException
137             */
138            private PropertyDescriptor[] introspectProperties() {
139    
140                    Method[] methods = beanClass.getMethods();
141                    List<Method> methodList = new ArrayList<Method>();
142                    
143                    for (Method method : methods) {
144                            if (!Modifier.isPublic(method.getModifiers()) || Modifier.isStatic(method.getModifiers()))
145                                    continue;
146                            methodList.add(method);
147                    }
148    
149                Map<String, Map<String, Object>> propertyMap = new HashMap<String, Map<String, Object>>(methodList.size());
150    
151                // Search for methods that either get or set a Property
152                for (Method method : methodList) {
153                    introspectGet(method, propertyMap);
154                    introspectSet(method, propertyMap);
155                }
156    
157                // fix possible getter & setter collisions
158                fixGetSet(propertyMap);
159                
160                // Put the properties found into the PropertyDescriptor array
161                List<PropertyDescriptor> propertyList = new ArrayList<PropertyDescriptor>();
162    
163                for (Map.Entry<String, Map<String, Object>> entry : propertyMap.entrySet()) {
164                    String propertyName = entry.getKey();
165                    Map<String, Object> table = entry.getValue();
166                    if (table == null)
167                        continue;
168                    
169                    Method getter = (Method)table.get("getter");
170                    Method setter = (Method)table.get("setter");
171    
172                    PropertyDescriptor propertyDesc = new PropertyDescriptor(propertyName, getter, setter);
173                    propertyList.add(propertyDesc);
174                }
175    
176                PropertyDescriptor[] properties = new PropertyDescriptor[propertyList.size()];
177                propertyList.toArray(properties);
178                return properties;
179            }
180    
181            @SuppressWarnings("unchecked")
182            private static void introspectGet(Method method, Map<String, Map<String, Object>> propertyMap) {
183                String methodName = method.getName();
184                
185                if (!(method.getName().startsWith("get") || method.getName().startsWith("is")))
186                    return;
187                
188                if (method.getParameterTypes().length > 0 || method.getReturnType() == void.class)
189                    return;
190                
191                if (method.getName().startsWith("is") && method.getReturnType() != boolean.class)
192                    return;
193    
194                String propertyName = method.getName().startsWith("get") ? methodName.substring(3) : methodName.substring(2);
195                propertyName = decapitalize(propertyName);
196    
197                Map<String, Object> table = propertyMap.get(propertyName);
198                if (table == null) {
199                    table = new HashMap<String, Object>();
200                    propertyMap.put(propertyName, table);
201                }
202    
203                List<Method> getters = (List<Method>)table.get("getters");
204                if (getters == null) {
205                    getters = new ArrayList<Method>();
206                    table.put("getters", getters);
207                }
208                getters.add(method);
209            }
210    
211            @SuppressWarnings("unchecked")
212            private static void introspectSet(Method method, Map<String, Map<String, Object>> propertyMap) {
213                String methodName = method.getName();
214                
215                if (!method.getName().startsWith("set"))
216                    return;
217                
218                if (method.getParameterTypes().length != 1 || method.getReturnType() != void.class)
219                    return;
220    
221                String propertyName = decapitalize(methodName.substring(3));
222    
223                Map<String, Object> table = propertyMap.get(propertyName);
224                if (table == null) {
225                    table = new HashMap<String, Object>();
226                    propertyMap.put(propertyName, table);
227                }
228    
229                List<Method> setters = (List<Method>)table.get("setters");
230                if (setters == null) {
231                    setters = new ArrayList<Method>();
232                    table.put("setters", setters);
233                }
234    
235                // add new setter
236                setters.add(method);
237            }
238    
239            /**
240             * Checks and fixs all cases when several incompatible checkers / getters
241             * were specified for single property.
242             * 
243             * @param propertyTable
244             * @throws IntrospectionException
245             */
246            private void fixGetSet(Map<String, Map<String, Object>> propertyMap) {
247                if (propertyMap == null)
248                    return;
249    
250                for (Entry<String, Map<String, Object>> entry : propertyMap.entrySet()) {
251                    Map<String, Object> table = entry.getValue();
252                    @SuppressWarnings("unchecked")
253                                    List<Method> getters = (List<Method>)table.get("getters");
254                    @SuppressWarnings("unchecked")
255                                    List<Method> setters = (List<Method>)table.get("setters");
256                    if (getters == null)
257                        getters = new ArrayList<Method>();
258                    if (setters == null)
259                        setters = new ArrayList<Method>();
260    
261                    Method definedGetter = getters.isEmpty() ? null : getters.get(0);
262                    Method definedSetter = null;
263    
264                    if (definedGetter != null) {
265                        Class<?> propertyType = definedGetter.getReturnType();
266            
267                        for (Method setter : setters) {
268                            if (setter.getParameterTypes().length == 1 && propertyType.equals(setter.getParameterTypes()[0])) {
269                                definedSetter = setter;
270                                break;
271                            }
272                        }
273                        if (definedSetter != null && !setters.isEmpty())
274                            definedSetter = setters.get(0);
275                    } 
276                    else if (!setters.isEmpty()) {
277                            definedSetter = setters.get(0);
278                    }
279    
280                    table.put("getter", definedGetter);
281                    table.put("setter", definedSetter);
282                }
283            }
284        }
285    }
286    
287