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