/** * == @Spearal ==> * * Copyright (C) 2014 Franck WOLFF & William DRAI (http://www.spearal.io) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.spearal.impl.introspector; import static java.lang.reflect.Modifier.PRIVATE; import static java.lang.reflect.Modifier.PROTECTED; import static java.lang.reflect.Modifier.STATIC; import static java.lang.reflect.Modifier.TRANSIENT; import static org.spearal.configuration.PropertyFactory.ZERO_PROPERTIES; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.logging.Logger; import org.spearal.SpearalContext; import org.spearal.annotation.Exclude; import org.spearal.annotation.Include; import org.spearal.configuration.Introspector; import org.spearal.configuration.PropertyFactory.Property; import org.spearal.impl.cache.AnyMap.ValueProvider; import org.spearal.impl.cache.CopyOnWriteMap; /** * @author Franck WOLFF */ public class IntrospectorImpl implements Introspector { private static Logger logger = Logger.getLogger(IntrospectorImpl.class.getName()); private final CopyOnWriteMap<Class<?>, Object, Property[]> cache; public IntrospectorImpl() { this.cache = new CopyOnWriteMap<Class<?>, Object, Property[]>(true, new ValueProvider<Class<?>, Object, Property[]>() { @Override public Property[] createValue(SpearalContext context, Class<?> key, Object unused) { return ( Proxy.isProxyClass(key) ? introspectProxyProperties(context, key) : introspectBeanProperties(context, key) ); } } ); } @Override public Property[] getProperties(SpearalContext context, Class<?> cls) { Property[] properties = cache.get(cls); if (properties == null) properties = cache.putIfAbsent(context, cls, null); return properties; } protected Property[] introspectBeanProperties(SpearalContext context, Class<?> cls) { if (cls == Object.class || cls == null) return ZERO_PROPERTIES; Field[] declaredFields = cls.getDeclaredFields(); Method[] declaredMethods = cls.getDeclaredMethods(); SortedMap<String, Property> propertiesMap = new TreeMap<String, Property>(); for (Field field : declaredFields) { if ((field.getModifiers() & (STATIC | TRANSIENT)) == 0 && !field.isAnnotationPresent(Exclude.class)) { field.setAccessible(true); String name = field.getName(); Class<?> type = field.getType(); Method getter = findGetter(declaredMethods, type, name); Method setter = findSetter(declaredMethods, type, name); Property property = context.createProperty(name, field, getter, setter); propertiesMap.put(name, property); } } for (Method getter : declaredMethods) { if ((getter.getModifiers() & (STATIC | PRIVATE | PROTECTED)) == 0 && getter.getReturnType() != void.class && getter.isAnnotationPresent(Include.class) && getter.getParameterTypes().length == 0) { String name = null; String methodName = getter.getName(); if (getter.getReturnType() == boolean.class && methodName.startsWith("is")) name = decapitalize(methodName.substring(2)); else if (methodName.startsWith("get")) name = decapitalize(methodName.substring(3)); if (name == null || name.length() == 0) { logger.warning("Ignoring method annotated with @Include (illegal property getter name): " + methodName); continue; } if (propertiesMap.containsKey(name)) continue; Class<?> type = getter.getReturnType(); Method setter = findSetter(declaredMethods, type, name); Property property = context.createProperty(name, null, getter, setter); propertiesMap.put(name, property); } } Property[] properties = propertiesMap.values().toArray(ZERO_PROPERTIES); Class<?> superCls = cls.getSuperclass(); if (superCls == Object.class || superCls == null) return properties; Property[] superProperties = getProperties(context, superCls); return concat(superProperties, properties); } protected Method findGetter(Method[] methods, Class<?> type, String name) { final String isName = "is" + name; final String getName = "get" + name; Method looseMatch = null; for (Method method : methods) { if ((method.getModifiers() & (STATIC | PRIVATE | PROTECTED)) == 0 && method.getReturnType() == type && method.getParameterTypes().length == 0) { if (type == boolean.class && method.getName().equalsIgnoreCase(isName)) { if (type.equals(decapitalize(method.getName().substring(2)))) return method; looseMatch = method; } else if (method.getName().equalsIgnoreCase(getName)) { if (name.equals(decapitalize(method.getName().substring(3)))) return method; looseMatch = method; } } } return looseMatch; } protected Method findSetter(Method[] methods, Class<?> type, String name) { final String setName = "set" + name; Method looseMatch = null; for (Method method : methods) { if ((method.getModifiers() & (STATIC | PRIVATE | PROTECTED)) == 0 && method.getReturnType() == void.class) { Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 1 && parameterTypes[0] == type && method.getName().equalsIgnoreCase(setName)) { if (name.equals(decapitalize(method.getName().substring(3)))) return method; looseMatch = method; } } } return looseMatch; } protected Property[] introspectProxyProperties(SpearalContext context, Class<?> cls) { if (cls == null) return ZERO_PROPERTIES; Map<String, Method> setters = new HashMap<String, Method>(); Map<String, Method> getters = new HashMap<String, Method>(); for (Class<?> inter : cls.getInterfaces()) { for (Method method : inter.getMethods()) { if (method.getReturnType() == void.class) { String name = method.getName(); if (name.length() > 3 && name.startsWith("set")) { Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 1) setters.put(name.substring(3).toLowerCase(), method); } } else if (method.getParameterTypes().length == 0) { String name = method.getName(); if (name.length() > 3 && name.startsWith("get")) getters.put(name.substring(3).toLowerCase(), method); else if (method.getReturnType() == boolean.class && name.length() > 2 && name.startsWith("is")) getters.put(name.substring(2).toLowerCase(), method); } } } SortedMap<String, Property> propertiesMap = new TreeMap<String, Property>(); for (Map.Entry<String, Method> nameGetter : getters.entrySet()) { Method getter = nameGetter.getValue(); Method setter = setters.get(nameGetter.getKey()); if ((setter != null && setter.getParameterTypes()[0] == getter.getReturnType()) || getter.isAnnotationPresent(Include.class)) { String name = ( getter.getName().startsWith("is") ? decapitalize(getter.getName().substring(2)) : decapitalize(getter.getName().substring(3)) ); Property property = context.createProperty(name, null, getter, setter); propertiesMap.put(name, property); } } return propertiesMap.values().toArray(ZERO_PROPERTIES); } protected static Property[] concat(Property[] properties1, Property[] properties2) { int length1 = properties1.length, length2 = properties2.length; if (length1 == 0) return properties2; if (length2 == 0) return properties1; Property[] properties = new Property[length1 + length2]; System.arraycopy(properties1, 0, properties, 0, length1); System.arraycopy(properties2, 0, properties, length1, length2); return properties; } private static String decapitalize(String name) { if (name == null || name.length() == 0) return name; if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) return name; char chars[] = name.toCharArray(); chars[0] = Character.toLowerCase(chars[0]); return new String(chars); } }