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