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