/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * Granite Data Services is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or see <http://www.gnu.org/licenses/>. */ package org.granite.util; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.WeakHashMap; /** * Basic bean introspector * Required for Android environment which does not include java.beans.Intropector */ public class Introspector { private static Map<Class<?>, PropertyDescriptor[]> descriptorCache = Collections.synchronizedMap(new WeakHashMap<Class<?>, PropertyDescriptor[]>(128)); /** * Decapitalizes a given string according to the rule: * <ul> * <li>If the first or only character is Upper Case, it is made Lower Case * <li>UNLESS the second character is also Upper Case, when the String is * returned unchanged <eul> * * @param name - * the String to decapitalize * @return the decapitalized version of the String */ public static String decapitalize(String name) { if (name == null) return null; // The rule for decapitalize is that: // If the first letter of the string is Upper Case, make it lower case // UNLESS the second letter of the string is also Upper Case, in which case no // changes are made. if (name.length() == 0 || (name.length() > 1 && Character.isUpperCase(name.charAt(1)))) { return name; } char[] chars = name.toCharArray(); chars[0] = Character.toLowerCase(chars[0]); return new String(chars); } /** * Flushes all <code>BeanInfo</code> caches. * */ public static void flushCaches() { // Flush the cache by throwing away the cache HashMap and creating a // new empty one descriptorCache.clear(); } /** * Flushes the <code>BeanInfo</code> caches of the specified bean class * * @param clazz * the specified bean class */ public static void flushFromCaches(Class<?> clazz) { if (clazz == null) throw new NullPointerException(); descriptorCache.remove(clazz); } /** * Gets the <code>BeanInfo</code> object which contains the information of * the properties, events and methods of the specified bean class. * * <p> * The <code>Introspector</code> will cache the <code>BeanInfo</code> * object. Subsequent calls to this method will be answered with the cached * data. * </p> * * @param beanClass * the specified bean class. * @return the <code>BeanInfo</code> of the bean class. * @throws IntrospectionException */ public static PropertyDescriptor[] getPropertyDescriptors(Class<?> beanClass) { PropertyDescriptor[] descriptor = descriptorCache.get(beanClass); if (descriptor == null) { descriptor = new BeanInfo(beanClass).getPropertyDescriptors(); descriptorCache.put(beanClass, descriptor); } return descriptor; } private static class BeanInfo { private Class<?> beanClass; private PropertyDescriptor[] properties = null; public BeanInfo(Class<?> beanClass) { this.beanClass = beanClass; if (properties == null) properties = introspectProperties(); } public PropertyDescriptor[] getPropertyDescriptors() { return properties; } /** * Introspects the supplied class and returns a list of the Properties of * the class * * @return The list of Properties as an array of PropertyDescriptors * @throws IntrospectionException */ private PropertyDescriptor[] introspectProperties() { Method[] methods = beanClass.getMethods(); List<Method> methodList = new ArrayList<Method>(); for (Method method : methods) { if (!Modifier.isPublic(method.getModifiers()) || Modifier.isStatic(method.getModifiers())) continue; methodList.add(method); } Map<String, Map<String, Object>> propertyMap = new HashMap<String, Map<String, Object>>(methodList.size()); // Search for methods that either get or set a Property for (Method method : methodList) { introspectGet(method, propertyMap); introspectSet(method, propertyMap); } // fix possible getter & setter collisions fixGetSet(propertyMap); // Put the properties found into the PropertyDescriptor array List<PropertyDescriptor> propertyList = new ArrayList<PropertyDescriptor>(); for (Map.Entry<String, Map<String, Object>> entry : propertyMap.entrySet()) { String propertyName = entry.getKey(); Map<String, Object> table = entry.getValue(); if (table == null) continue; Method getter = (Method)table.get("getter"); Method setter = (Method)table.get("setter"); PropertyDescriptor propertyDesc = new PropertyDescriptor(propertyName, getter, setter); propertyList.add(propertyDesc); } PropertyDescriptor[] properties = new PropertyDescriptor[propertyList.size()]; propertyList.toArray(properties); return properties; } @SuppressWarnings("unchecked") private static void introspectGet(Method method, Map<String, Map<String, Object>> propertyMap) { String methodName = method.getName(); if (!(method.getName().startsWith("get") || method.getName().startsWith("is"))) return; if (method.getParameterTypes().length > 0 || method.getReturnType() == void.class) return; if (method.getName().startsWith("is") && method.getReturnType() != boolean.class) return; String propertyName = method.getName().startsWith("get") ? methodName.substring(3) : methodName.substring(2); propertyName = decapitalize(propertyName); Map<String, Object> table = propertyMap.get(propertyName); if (table == null) { table = new HashMap<String, Object>(); propertyMap.put(propertyName, table); } List<Method> getters = (List<Method>)table.get("getters"); if (getters == null) { getters = new ArrayList<Method>(); table.put("getters", getters); } getters.add(method); } @SuppressWarnings("unchecked") private static void introspectSet(Method method, Map<String, Map<String, Object>> propertyMap) { String methodName = method.getName(); if (!method.getName().startsWith("set")) return; if (method.getParameterTypes().length != 1 || method.getReturnType() != void.class) return; String propertyName = decapitalize(methodName.substring(3)); Map<String, Object> table = propertyMap.get(propertyName); if (table == null) { table = new HashMap<String, Object>(); propertyMap.put(propertyName, table); } List<Method> setters = (List<Method>)table.get("setters"); if (setters == null) { setters = new ArrayList<Method>(); table.put("setters", setters); } // add new setter setters.add(method); } /** * Checks and fixs all cases when several incompatible checkers / getters * were specified for single property. * * @param propertyTable * @throws IntrospectionException */ private void fixGetSet(Map<String, Map<String, Object>> propertyMap) { if (propertyMap == null) return; for (Entry<String, Map<String, Object>> entry : propertyMap.entrySet()) { Map<String, Object> table = entry.getValue(); @SuppressWarnings("unchecked") List<Method> getters = (List<Method>)table.get("getters"); @SuppressWarnings("unchecked") List<Method> setters = (List<Method>)table.get("setters"); if (getters == null) getters = new ArrayList<Method>(); if (setters == null) setters = new ArrayList<Method>(); Method definedGetter = getters.isEmpty() ? null : getters.get(0); Method definedSetter = null; if (definedGetter != null) { Class<?> propertyType = definedGetter.getReturnType(); for (Method setter : setters) { if (setter.getParameterTypes().length == 1 && propertyType.equals(setter.getParameterTypes()[0])) { definedSetter = setter; break; } } if (definedSetter != null && !setters.isEmpty()) definedSetter = setters.get(0); } else if (!setters.isEmpty()) { definedSetter = setters.get(0); } table.put("getter", definedGetter); table.put("setter", definedSetter); } } } }