package net.sourceforge.javautil.common.reflection.cache;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sourceforge.javautil.common.ReflectionUtil;
import net.sourceforge.javautil.common.exception.ThrowableManagerRegistry;
import net.sourceforge.javautil.common.reflection.ReflectionContext;
import net.sourceforge.javautil.common.reflection.IReflectionManager;
import net.sourceforge.javautil.common.reflection.cache.ClassMethodAbstract.ArgumentMatch;
/**
* A class descriptor containing information on what public properties
* and methods are available on a particular class. Also providing
* many helper classes and methods for dealing with common reflection
* operations.
*
* @author elponderador
* @author $Author: ponderator $
* @version $Id: ClassDescriptor.java 2722 2011-01-16 05:38:59Z ponderator $
*/
public class ClassDescriptor<T> {
protected final ClassCache cache;
protected final Class<T> clazz;
protected final List<ClassConstructor<T>> constructors = new ArrayList<ClassConstructor<T>>();
protected final Map<String, List<ClassMethod>> methods = new LinkedHashMap<String, List<ClassMethod>>();
protected final Map<String, ClassProperty> properties = new LinkedHashMap<String, ClassProperty>();
protected final Map<String, List<ClassField>> fields = new LinkedHashMap<String, List<ClassField>>();
protected List<ClassMethod> getters = new ArrayList<ClassMethod>();
protected Boolean initializedFields;
protected Boolean initializedProperties;
protected Boolean initializedMethods;
protected Boolean initializedConstructors;
/**
* All the description information will be loaded at instantiation time.
*
* @param clazz The class for which to create a descriptor
*/
public ClassDescriptor(ClassCache cache, Class<T> clazz) {
this.clazz = clazz;
this.cache = cache;
}
/**
* @return The class this descriptor is for
*/
public Class<T> getDescribedClass () { return this.clazz; }
/**
* @return An array, possibly empty, of publicly accessible properties
*/
public String[] getPropertyNames () {
this.detectProperties();
String[] names = new String[properties.size()];
Iterator<String> keys = this.properties.keySet().iterator();
for (int i=0; keys.hasNext(); i++) { names[i] = keys.next(); }
return names;
}
/**
* @return An array, possibly empty, of publicly accessible fields
*/
public String[] getFieldNames () {
this.detectFields();
String[] names = new String[fields.size()];
Iterator<String> keys = this.fields.keySet().iterator();
for (int i=0; keys.hasNext(); i++) { names[i] = keys.next(); }
return names;
}
/**
* @param <A> The type of annotation
* @param clazz The annotation class
* @return The annotation if present on the class, otherwise null
*/
public <A extends Annotation> A getAnnotation (Class<A> clazz) {
return this.clazz.getAnnotation(clazz);
}
/**
* @return True if this describes an array, otherwise false
*/
public boolean isArray () { return this.clazz.isArray(); }
/**
* @param name The name of a property
* @return True if the class does have this public property, otherwise false
*/
public boolean hasProperty (String name) {
this.detectProperties();
return this.properties.containsKey(name);
}
/**
* @param name The name of a method
* @return True if the class does have this public method, otherwise false
*/
public boolean hasMethod (String name) {
this.detectMethods();
return this.methods.containsKey(name);
}
/**
* @return The class cache where this descriptor resides
*/
public ClassCache getCache() { return cache; }
/**
* @return True if this descriptor describes one of the primitive types, a boxed primitive or a {@link String}
*/
public boolean isBasicType () {
return clazz.isPrimitive() ||
clazz == Integer.class ||
clazz == Long.class ||
clazz == Short.class ||
clazz == Double.class ||
clazz == Float.class ||
clazz == Boolean.class ||
clazz == Byte.class ||
clazz == Character.class ||
clazz == String.class;
}
/**
* @return The component type of the array if this describes an array, otherwise null
*/
public ClassDescriptor getComponentTypeDescriptor () {
return clazz.isArray() ? cache.getDescriptor(clazz.getComponentType()) : null;
}
/**
* The key of the map will be the name of the method, and the collection
* will be one or more methods with different signatures.
*
* @return All the public methods
*/
public Map<String, List<ClassMethod>> getMethods () {
this.detectMethods();
return new LinkedHashMap<String, List<ClassMethod>>(this.methods);
}
/**
* @param name The name of the method
* @return An array of methods with the same name, but different signatures, or an empty array if no such method exists
*/
public ClassMethod[] getMethods (String name) {
this.detectMethods();
List<ClassMethod> methods = this.methods.get(name);
return methods == null ? new ClassMethod[0] : methods.toArray(new ClassMethod[methods.size()]);
}
/**
* @param name The name of the field
* @return The field corresponding to the name, or null if no such field exists
*/
public ClassField getField (String name) {
this.detectFields();
return this.fields.containsKey(name) ? this.fields.get(name).get(0) : null;
}
/**
* The key will be the name of the field and the value the wrapper for
* accessing it.
*
* @return A map of all public fields
*/
public Map<String, List<ClassField>> getFields () {
this.detectFields();
return new LinkedHashMap<String, List<ClassField>>(this.fields);
}
public int getFieldCount () {
this.detectFields();
return this.fields.size();
}
/**
* @param type The type of annotation to search for
* @return The first field found that has the annotation declared on it
*/
public ClassField getField (Class<? extends Annotation> type) {
this.detectFields();
for (String name : fields.keySet()) {
for (ClassField field : fields.get(name)) {
if (field.getAnnotation(type) != null) return field;
}
}
return null;
}
/**
* @param type The type of annotation to search for
* @return An array, possibly empty, of fields that have the annotation declared on them
*/
public ClassField[] getFields (Class<? extends Annotation> type) {
this.detectFields();
List<ClassField> fields = new ArrayList<ClassField>();
for (String name : this.fields.keySet()) {
for (ClassField field : this.fields.get(name)) {
if (field.field.getAnnotation(type) != null) fields.add(field);
}
}
return fields.toArray(new ClassField[fields.size()]);
}
/**
* The key will be the Java Bean property name, and the object will
* allow for access to, setting and getting on the property.
*
* @return All the public properties
*/
public Map<String, ClassProperty> getProperties () {
this.detectProperties();
return new LinkedHashMap<String, ClassProperty>(this.properties);
}
/**
* @param instance The instance for which to get properties
* @return A map of property names<->instance values
*/
public Map<String, Object> getProperties (Object instance) {
this.detectProperties();
Map<String, Object> values = new LinkedHashMap<String, Object>();
for (String name : this.properties.keySet()) {
values.put(name, this.properties.get(name).getValue(instance));
}
return values;
}
/**
* @param name The name of the property
* @return The property corresponding to the name, or null if no such property exists
*/
public ClassProperty getProperty (String name) {
this.detectProperties();
return this.properties.get(name);
}
/**
* @param annotation The annotation class to check for
* @return An array, possibly empty, of methods that have the specified annotation
*/
public ClassMethod[] getMethods (Class<? extends Annotation> annotation) {
this.detectMethods();
List<ClassMethod> methods = new ArrayList<ClassMethod>();
for (String name : this.methods.keySet()) {
List<ClassMethod> nm = this.methods.get(name);
for (ClassMethod method : nm) {
if (method.getJavaMember().getAnnotation(annotation) != null)
methods.add(method);
}
}
return methods.toArray(new ClassMethod[methods.size()]);
}
/**
* @param annotation The annotation class to check for
* @return The first method found with the annotation, or null if no method has it
*/
public ClassMethod getMethod (Class<? extends Annotation> annotation) {
this.detectMethods();
for (String name : this.methods.keySet()) {
List<ClassMethod> nm = this.methods.get(name);
for (ClassMethod method : nm) {
if (method.getJavaMember().getAnnotation(annotation) != null)
return method;
}
}
return null;
}
/**
* @param annotation The annotation class to check for
* @return The first property found with this annotation, or null if there are none
*/
public ClassProperty getProperty (Class<? extends Annotation> annotation) {
this.detectProperties();
for (String name : this.properties.keySet()) {
ClassProperty property = this.properties.get(name);
if (property.getAnnotation(annotation) != null)
return property;
}
return null;
}
/**
* @param annotation The annotation class to check for
* @return An array, possibly empty, of the properties that have the specified annotation
*/
public ClassProperty[] getProperties (Class<? extends Annotation> annotation) {
this.detectProperties();
List<ClassProperty> properties = new ArrayList<ClassProperty>();
for (String name : this.properties.keySet()) {
ClassProperty property = this.properties.get(name);
if (property.getAnnotation(annotation) != null)
properties.add(property);
}
return properties.toArray(new ClassProperty[properties.size()]);
}
/**
* @param arguments The arguments to use for the constructor
* @return A class constructor, or null if it could not be found
*/
public ClassConstructor<T> getConstructor (Object... arguments) {
this.detectConstructors();
return this.getClosestMatchInternal(this.constructors, arguments);
}
/**
* @param parameterTypes The parameter types
* @return A class constructor, or null if none matched
*/
public ClassConstructor<T> getConstructor (Class<?>... parameterTypes) {
this.detectConstructors();
return this.getClosestMatchInternal(this.constructors, parameterTypes);
}
/**
* @param annotation The annotation type to look for
* @return The first constructor that was annotated with this annotation, or null if none were
*/
public ClassConstructor<T> getConstructor (Class<? extends Annotation> annotation) {
this.detectConstructors();
for (ClassConstructor<T> constructor : this.constructors) {
if (constructor.getAnnotation(annotation) != null) return constructor;
}
return null;
}
/**
* @return The constructors publicly available for this descriptor
*/
public List<ClassConstructor<T>> getConstructors () {
this.detectConstructors();
return new ArrayList<ClassConstructor<T>>(this.constructors);
}
/**
* Create a new instance of the class.
*
* @param arguments The arguments to use for creating the class
* @return The instance that was created
*/
public T newInstance (Object... arguments) {
if (clazz.isArray()) {
boolean valid = true;
int[] indices = new int[arguments.length];
for (int a=0; a<arguments.length; a++) {
Object arg = arguments[a];
if (arg != null && arg.getClass() == Integer.class) {
indices[a] = ((Integer)arg).intValue();
continue;
}
valid = false; break;
}
if (!valid || indices.length == 0)
indices = new int[] { 0 };
return this.createArray(indices);
}
this.detectConstructors();
ClassConstructor<T> constructor = this.getClosestMatchInternal(this.constructors, arguments);
if (constructor != null) return constructor.newInstance(arguments);
throw new ClassInstantiationException(this, arguments, "No matching constructor could be found");
}
protected T createArray (int... indices) {
return (T) Array.newInstance(clazz.getComponentType(), indices);
}
/**
* @param <E> The type of object
* @param instance The instance of the class, or null if this is a static property
* @param name The name of the property
* @return The current value of the property
*/
public <E extends T> Object getPropertyValue (E instance, String name) {
this.detectProperties();
if (!this.properties.containsKey(name))
throw new ClassPropertyNotFoundException(this, name);
return this.properties.get(name).getValue(instance);
}
/**
* This is the complement of {@link #deserializeProperties(Object, Map)}.
*
* @param instance The instance to serialize properties for
* @return
*/
public Map<String, String> serializeProperties (Object instance) {
this.detectProperties();
Map<String, String> settings = new LinkedHashMap<String, String>();
Map<String, ClassProperty> properties = getProperties();
for (String name : properties.keySet()) {
settings.put(name, ReflectionUtil.coerce(String.class, properties.get(name).getValue(this)));
}
return settings;
}
/**
* The keys on the map should correspond to properties that really
* exist and the values should be appropriate values for the respective
* property.
*
* @param instance The instance on which to set the properties
* @param properties The properties to set on the instance
*/
public void deserializeProperties (T instance, Map<String, Object> properties) {
this.detectProperties();
for (String name : properties.keySet()) {
ClassProperty property = this.getProperty(name);
if (!property.isWritable())
throw new ClassPropertyAccessException(this, property, "Cannot set read-only property:" + name);
property.setValue(instance, ReflectionUtil.coerce(property.getType(), properties.get(name)));
}
}
/**
* Set/assign a value to the property.
*
* @param <E> The type of object
* @param instance The instance of the class, or null if this is a static property
* @param name The name of the property
* @param value The value to set/assign
*/
public <E extends T> void setPropertyValue (E instance, String name, Object value) {
this.detectProperties();
if (!this.properties.containsKey(name))
throw new ClassPropertyNotFoundException(this, name);
ClassProperty property = this.properties.get(name);
property.setValue(instance, ReflectionUtil.coerce(property.getType(), value));
}
/**
* @param name The name of the method to invoke
* @param instance The instance on which to invoke it, or null if this is a static call
* @param arguments The arguments to pass to the method invocation
* @return The value returned by the method
*/
public Object invoke (String name, Object instance, Object... arguments) {
this.detectMethods();
if (!this.methods.containsKey(name))
throw new ClassDescriptorException(this, "No such method exists: " + name);
ClassMethod method = this.getClosestMatchInternal(this.methods.get(name), arguments);
if (method == null)
throw new ClassDescriptorException(this, "No such method exists for arguments provided: " + name);
return method.invoke(instance, arguments);
}
public String toString () { return "ClassDescriptor[" + clazz + "]"; }
/**
* @param name The name of the method
* @param types The parameter types
* @return The method that most closely matches the types, or null if no such method exists
*/
public ClassMethod findMethod (String name, Class... types) {
this.detectMethods();
return this.getClosestMatch(this.methods.get(name), types);
}
/**
* @param name The name of the method
* @param arguments The arguments for the method
* @return The method that most closely matches the arguments, or null if no such method exists
*/
public ClassMethod findMethod (String name, Object... arguments) {
this.detectMethods();
return this.getClosestMatch(this.methods.get(name), arguments);
}
/**
* @param <M> The type of method
* @param members The collection to search
* @param arguments The arguments to search against
* @return A method/constructor that should work with the arguments, otherwise null
*/
public <M extends ClassMethodAbstract> M getClosestMatch (Collection<M> members, Object... arguments) {
this.detectMethods();
return this.getClosestMatchInternal(members, arguments);
}
protected <M extends ClassMethodAbstract> M getClosestMatchInternal (Collection<M> members, Object... arguments) {
if (members == null) return null;
M match = null;
synchronized (members) {
for (M member : members) {
ArgumentMatch result = member.compareArguments(arguments);
if (result == ArgumentMatch.EXACT) return member;
else if (result == ArgumentMatch.FUNCTIONAL && match == null)
match = member;
}
return match;
}
}
/**
* @param <M> The type of method
* @param members The collection to search
* @param arguments The argument types to search against
* @return A method/constructor that should work with the argument types, otherwise null
*/
public <M extends ClassMethodAbstract> M getClosestMatch (Collection<M> members, Class... arguments) {
this.detectMethods();
return this.getClosestMatchInternal(members, arguments);
}
protected <M extends ClassMethodAbstract> M getClosestMatchInternal (Collection<M> members, Class... arguments) {
if (members == null) return null;
M match = null;
for (M member : members) {
ArgumentMatch result = member.compareArgumentTypes(arguments);
if (result == ArgumentMatch.EXACT) return member;
else if (result == ArgumentMatch.FUNCTIONAL && match == null)
match = member;
}
return match;
}
protected void detectFields () {
if (this.initializedFields != null && this.initializedFields) return;
synchronized (this.fields) {
if (this.initializedFields == Boolean.TRUE) return;
this.initializedFields = Boolean.FALSE;
Class sc = clazz;
IReflectionManager manager = ReflectionContext.getReflectionManager();
while (sc != null) {
for (Field field : manager.getDeclaredFields(sc)) {
if (!this.fields.containsKey(field.getName())) this.fields.put(field.getName(), new ArrayList<ClassField>());
this.fields.get(field.getName()).add( new ClassField(field, this) );
}
sc = sc.getSuperclass();
}
this.initializedFields = Boolean.TRUE;
}
}
protected void detectProperties () {
if (this.initializedProperties != null && this.initializedProperties) return;
synchronized (this.properties) {
if (this.initializedProperties == Boolean.TRUE) return;
this.initializedProperties = Boolean.FALSE;
this.detectMethods();
try {
// TODO: detect write only/setter only methods
for (ClassMethod getter : getters) {
String name = getter.getName().startsWith("is") ? getter.getName().substring(2) : getter.getName().substring(3);
String propertyName = name.length() > 1 ?
name.substring(0, 1).toLowerCase() + name.substring(1) :
name.toLowerCase();
ClassMethod setterMethod = null;
String setterName = "set" + name;
if (methods.containsKey(setterName)) {
setterMethod = this.getClosestMatchInternal(methods.get(setterName), getter.getReturnType());
}
this.properties.put(propertyName, new ClassProperty(this, propertyName, getter, setterMethod));
}
this.getters = null;
this.initializedProperties = Boolean.TRUE;
} catch (Exception e) {
this.initializedProperties = null;
throw ThrowableManagerRegistry.caught(e);
}
}
}
protected void detectMethods () {
if (this.initializedMethods != null && this.initializedMethods) return;
synchronized (this.methods) {
if (this.initializedMethods == Boolean.TRUE) return;
this.initializedMethods = Boolean.FALSE;
try {
IReflectionManager manager = ReflectionContext.getReflectionManager();
Set<Class<?>> supers = clazz.isInterface() ? ReflectionUtil.extractInterfaces(clazz) : ReflectionUtil.extractSuperclasses(clazz);
for (Class<?> sc : supers) {
for (Method method : manager.getDeclaredMethods(sc)) {
String name = method.getName();
ClassMethod cmethod = null;
if (!methods.containsKey(name)) methods.put(name, new ArrayList<ClassMethod>());
else {
boolean override = false;
List<ClassMethod> cms = new ArrayList<ClassMethod>(methods.get(name));
for (ClassMethod cm : cms) {
if (cm.getParameterTypes().length == method.getParameterTypes().length) {
boolean sameArguments = true;
for (int p=0; p<cm.getParameterTypes().length; p++) {
if (cm.getParameterTypes()[p] != method.getParameterTypes()[p]) {
sameArguments = false;
break;
}
}
if (!sameArguments) continue;
if (method.getReturnType().isAssignableFrom(cm.getReturnType())) {
override = true;
} else {
methods.get(name).remove(cm);
}
}
}
if (override) continue;
}
methods.get(name).add(cmethod = new ClassMethod(this, method));
if (method.getDeclaringClass() != Object.class &&
(
(name.startsWith("get") && name.length() > 3) ||
(name.startsWith("is") && name.length() > 2)
)
&& method.getParameterTypes().length == 0) {
getters.add( cmethod );
cmethod.propertyMethod = true;
}
}
this.initializedMethods = Boolean.TRUE;
sc = sc.getSuperclass();
}
} catch (Exception e) {
this.initializedMethods = null;
throw ThrowableManagerRegistry.caught(e);
}
}
}
/**
* Initialize the property, constructor and method wrappers
*/
protected void detectConstructors () {
if (this.initializedConstructors != null && this.initializedConstructors == Boolean.TRUE) return;
synchronized (this.constructors) {
if (this.initializedConstructors == Boolean.TRUE) return;
this.initializedConstructors = Boolean.FALSE;
try {
for (Constructor constructor : ReflectionContext.getReflectionManager().getDeclaredConstructors(clazz)) {
this.constructors.add(new ClassConstructor<T>(this, constructor));
}
this.initializedConstructors = Boolean.TRUE;
} catch (Exception e) {
this.initializedConstructors = null;
throw ThrowableManagerRegistry.caught(e);
}
}
}
}