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