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 */
022package org.granite.util;
023
024import java.lang.reflect.Method;
025import java.lang.reflect.Modifier;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.WeakHashMap;
033
034/**
035 *      Basic bean introspector
036 *  Required for Android environment which does not include java.beans.Intropector
037 */
038public 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