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