/* * Copyright 2004-2010 Brian S O'Neill * * 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.cojen.util; import java.lang.ref.SoftReference; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * Alternative to the standard Bean introspector. One key difference is that * this introspector ensures interface properties are properly * discovered. Also, indexed properties can have an index of any type. * * @author Brian S O'Neill */ public class BeanIntrospector { // Weakly maps Class objects to softly referenced BeanProperty maps. private static Map<Class, SoftReference<Map<String, BeanProperty>>> cPropertiesCache = new WeakIdentityMap<Class, SoftReference<Map<String, BeanProperty>>>(); public static void main(String[] args) throws Exception { System.out.println(getAllProperties(Class.forName(args[0]))); } /** * Returns a Map of all the available properties on a given class including * write-only and indexed properties. * * @return Map<String, BeanProperty> an unmodifiable mapping of property * names (Strings) to BeanProperty objects. * */ public static Map<String, BeanProperty> getAllProperties(Class clazz) { synchronized (cPropertiesCache) { Map<String, BeanProperty> properties; SoftReference<Map<String, BeanProperty>> ref = cPropertiesCache.get(clazz); if (ref != null) { properties = ref.get(); if (properties != null) { return properties; } } properties = createProperties(clazz); cPropertiesCache.put(clazz, new SoftReference<Map<String, BeanProperty>>(properties)); return properties; } } private static Map<String, BeanProperty> createProperties(Class clazz) { if (clazz == null || clazz.isPrimitive()) { return Collections.emptyMap(); } Map<String, BeanProperty> properties = new HashMap<String, BeanProperty>(); fillInProperties(clazz, properties); // Properties defined in Object are also available to interfaces. if (clazz.isInterface()) { fillInProperties(Object.class, properties); } // Ensure that all implemented interfaces are properly analyzed. Class[] interfaces = clazz.getInterfaces(); for (int i=0; i<interfaces.length; i++) { fillInProperties(interfaces[i], properties); } return Collections.unmodifiableMap(properties); } /** * @param clazz Class to introspect * @param properties Receives properties as name->property entries */ private static void fillInProperties(Class clazz, Map<String, BeanProperty> properties) { Method[] methods = clazz.getMethods(); Method method; String name; Class type; Class[] params; SimpleProperty property; IndexedProperty indexedProperty; // Gather non-conflicting "get" accessors for (int i=0; i<methods.length; i++) { method = methods[i]; if (Modifier.isStatic(method.getModifiers()) || (type = method.getReturnType()) == void.class || method.getParameterTypes().length > 0 || (name = extractPropertyName(method, "get")) == null) { continue; } if (properties.containsKey(name)) { property = (SimpleProperty) properties.get(name); SimpleProperty withCovariant = property.addCovariantType(type); if (withCovariant == null) { if (property.getReadMethod() != null) { continue; } } else { properties.put(name, property = withCovariant); if (type != property.getType() && property.getReadMethod() != null) { // Primary type not changed so don't change read method. continue; } } } else { property = new SimpleProperty(name, type); properties.put(name, property); } property.setReadMethod(method); } // Gather non-conflicting "is" accessors. for (int i=0; i<methods.length; i++) { method = methods[i]; if (Modifier.isStatic(method.getModifiers()) || (type = method.getReturnType()) != boolean.class || method.getParameterTypes().length > 0 || (name = extractPropertyName(method, "is")) == null) { continue; } if (properties.containsKey(name)) { property = (SimpleProperty) properties.get(name); if (type != property.getType() || property.getReadMethod() != null) { continue; } } else { property = new SimpleProperty(name, type); properties.put(name, property); } property.setReadMethod(method); } // Gather non-conflicting mutators. for (int i=0; i<methods.length; i++) { method = methods[i]; if (Modifier.isStatic(method.getModifiers()) || method.getReturnType() != void.class || (params = method.getParameterTypes()).length != 1 || (name = extractPropertyName(method, "set")) == null) { continue; } type = params[0]; if (properties.containsKey(name)) { property = (SimpleProperty) properties.get(name); SimpleProperty withCovariant = property.addCovariantType(type); if (withCovariant == null) { if (property.getWriteMethod() != null) { continue; } } else { properties.put(name, property = withCovariant); if (type != property.getType() && property.getWriteMethod() != null) { // Primary type not changed so don't change write method. continue; } } } else { property = new SimpleProperty(name, type); properties.put(name, property); } property.setWriteMethod(method); } // Gather non-conflicting indexed property accessors. for (int i=0; i<methods.length; i++) { method = methods[i]; if (Modifier.isStatic(method.getModifiers()) || (type = method.getReturnType()) == void.class || (params = method.getParameterTypes()).length != 1 || (name = extractPropertyName(method, "get")) == null) { continue; } if (properties.containsKey(name)) { property = (SimpleProperty) properties.get(name); if (type != property.getType()) { continue; } if (property instanceof IndexedProperty) { indexedProperty = (IndexedProperty) property; } else { indexedProperty = new IndexedProperty(property); properties.put(name, indexedProperty); } } else { indexedProperty = new IndexedProperty(name, type); properties.put(name, indexedProperty); } indexedProperty.addIndexedReadMethod(method); } // Gather non-conflicting indexed property mutators. for (int i=0; i<methods.length; i++) { method = methods[i]; if (Modifier.isStatic(method.getModifiers()) || method.getReturnType() != void.class || (params = method.getParameterTypes()).length != 2 || (name = extractPropertyName(method, "set")) == null) { continue; } type = params[1]; if (properties.containsKey(name)) { property = (SimpleProperty) properties.get(name); if (type != property.getType()) { continue; } if (property instanceof IndexedProperty) { indexedProperty = (IndexedProperty) property; } else { indexedProperty = new IndexedProperty(property); properties.put(name, indexedProperty); } } else { indexedProperty = new IndexedProperty(name, type); properties.put(name, indexedProperty); } indexedProperty.addIndexedWriteMethod(method); } } /** * Returns null if prefix pattern doesn't match */ private static String extractPropertyName(Method method, String prefix) { String name = method.getName(); if (!name.startsWith(prefix)) { return null; } if (name.length() == prefix.length()) { return ""; } name = name.substring(prefix.length()); if (!Character.isUpperCase(name.charAt(0)) || name.indexOf('$') >= 0) { return null; } // Decapitalize the name only if it doesn't begin with two uppercase // letters. if (name.length() == 1 || !Character.isUpperCase(name.charAt(1))) { char chars[] = name.toCharArray(); chars[0] = Character.toLowerCase(chars[0]); name = new String(chars); } return name.intern(); } private static class SimpleProperty implements BeanProperty { private static final Class[] EMPTY = new Class[0]; private final String mName; private final Class mType; private final Class[] mCovariantTypes; private Method mReadMethod; private Method mWriteMethod; SimpleProperty(String name, Class type) { this(name, type, null); } SimpleProperty(String name, Class type, Class[] covariantTypes) { mName = name; mType = type; if (covariantTypes == null || covariantTypes.length == 0) { mCovariantTypes = EMPTY; } else { mCovariantTypes = covariantTypes; } } public String getName() { return mName; } public Class getType() { return mType; } public Class[] getCovariantTypes() { if (mCovariantTypes.length == 0) { return EMPTY; } return mCovariantTypes.clone(); } public Method getReadMethod() { return mReadMethod; } public Method getWriteMethod() { return mWriteMethod; } public int getIndexTypesCount() { return 0; } public Class getIndexType(int index) { throw new IndexOutOfBoundsException(); } public Method getIndexedReadMethod(int index) { throw new IndexOutOfBoundsException(); } public Method getIndexedWriteMethod(int index) { throw new IndexOutOfBoundsException(); } public String toString() { String str = "BeanProperty{name=" + getName() + ", type=" + getType().getName(); Class[] covariantTypes = getCovariantTypes(); if (covariantTypes.length > 0) { str = str + ", covariantTypes=" + Arrays.toString(covariantTypes); } return str + '}'; } /** * @return null if no change */ SimpleProperty addCovariantType(Class type) { if (mType == type) { return null; } for (Class covariant : mCovariantTypes) { if (covariant == type) { return null; } } // Find most specialized type. Class newType = mType; if (mType.isAssignableFrom(type)) { newType = type; } for (Class covariant : mCovariantTypes) { if (covariant.isAssignableFrom(type)) { newType = type; } } Class[] newCovariant = new Class[1 + mCovariantTypes.length]; System.arraycopy(mCovariantTypes, 0, newCovariant, 1, mCovariantTypes.length); newCovariant[0] = newType == mType ? type : mType; SimpleProperty property = new SimpleProperty(mName, newType, newCovariant); property.mReadMethod = mReadMethod; property.mWriteMethod = mWriteMethod; return property; } void setReadMethod(Method method) { mReadMethod = method; } void setWriteMethod(Method method) { mWriteMethod = method; } } private static class IndexedProperty extends SimpleProperty { private Method[] mIndexedReadMethods; private Method[] mIndexedWriteMethods; IndexedProperty(String name, Class type) { super(name, type); } IndexedProperty(String name, Class type, Class[] covariantTypes) { super(name, type, covariantTypes); } IndexedProperty(BeanProperty property) { super(property.getName(), property.getType(), property.getCovariantTypes()); setReadMethod(property.getReadMethod()); setWriteMethod(property.getWriteMethod()); } public int getIndexTypesCount() { Method[] methods = mIndexedReadMethods; if (methods != null) { return methods.length; } methods = mIndexedWriteMethods; return methods == null ? 0 : methods.length; } public Class getIndexType(int index) { Method[] methods = mIndexedReadMethods; if (methods != null) { Method method = methods[0]; if (method != null) { return method.getParameterTypes()[0]; } } methods = mIndexedWriteMethods; if (methods != null) { Method method = methods[index]; if (method != null) { return method.getParameterTypes()[0]; } } if (index >= getIndexTypesCount()) { throw new IndexOutOfBoundsException(); } return null; } public Method getIndexedReadMethod(int index) { return mIndexedReadMethods[index]; } public Method getIndexedWriteMethod(int index) { return mIndexedWriteMethods[index]; } public String toString() { StringBuffer buf = new StringBuffer(); buf.append("BeanProperty{name="); buf.append(getName()); buf.append(", type="); buf.append(getType().getName()); Class[] covariantTypes = getCovariantTypes(); if (covariantTypes.length > 0) { buf.append(", covariantTypes="); buf.append(Arrays.toString(covariantTypes)); } buf.append(", "); int count = getIndexTypesCount(); for (int i=0; i<count; i++) { if (i > 0) { buf.append(", "); } buf.append("indexType["); buf.append(i); buf.append("]="); buf.append(getIndexType(0)); } buf.append('}'); return buf.toString(); } IndexedProperty addCovariantType(Class type) { SimpleProperty property = super.addCovariantType(type); if (property == null) { return null; } IndexedProperty ix = new IndexedProperty(property); ix.mIndexedReadMethods = mIndexedReadMethods; ix.mIndexedWriteMethods = mIndexedWriteMethods; return ix; } void addIndexedReadMethod(Method method) { Class indexType = method.getParameterTypes()[0]; int count = getIndexTypesCount(); int i; for (i=0; i<count; i++) { if (getIndexType(i) == indexType) { break; } } if (i >= count) { expandCapactity(); } if (mIndexedReadMethods[i] == null) { mIndexedReadMethods[i] = method; } } void addIndexedWriteMethod(Method method) { Class indexType = method.getParameterTypes()[0]; int count = getIndexTypesCount(); int i; for (i=0; i<count; i++) { if (getIndexType(i) == indexType) { break; } } if (i >= count) { expandCapactity(); } if (mIndexedWriteMethods[i] == null) { mIndexedWriteMethods[i] = method; } } private void expandCapactity() { int count = getIndexTypesCount(); Method[] methods = new Method[count + 1]; if (mIndexedReadMethods != null) { System.arraycopy(mIndexedReadMethods, 0, methods, 0, count); } mIndexedReadMethods = methods; methods = new Method[count + 1]; if (mIndexedWriteMethods != null) { System.arraycopy(mIndexedWriteMethods, 0, methods, 0, count); } mIndexedWriteMethods = methods; } } }