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