/**
* $Id: ClassFields.java 129 2014-03-18 23:25:36Z azeckoski $
* $URL: http://reflectutils.googlecode.com/svn/trunk/src/main/java/org/azeckoski/reflectutils/ClassFields.java $
* ClassFields.java - genericdao - May 5, 2008 2:16:35 PM - azeckoski
**************************************************************************
* Copyright (c) 2008 Aaron Zeckoski
* Licensed under the Apache License, Version 2
*
* A copy of the Apache License, Version 2 has been included in this
* distribution and is available at: http://www.apache.org/licenses/LICENSE-2.0.txt
*
* Aaron Zeckoski (azeckoski@gmail.com) (aaronz@vt.edu) (aaron@caret.cam.ac.uk)
*/
package org.azeckoski.reflectutils;
import org.azeckoski.reflectutils.annotations.ReflectIgnoreClassFields;
import org.azeckoski.reflectutils.annotations.ReflectIncludeStaticFields;
import org.azeckoski.reflectutils.annotations.ReflectTransientClassFields;
import org.azeckoski.reflectutils.exceptions.FieldnameNotFoundException;
import org.azeckoski.reflectutils.map.ArrayOrderedMap;
import org.azeckoski.reflectutils.map.OrderedMap;
import java.beans.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.Map.Entry;
/**
* @param <T> the class type
* This is used as a record (cache) of the information about a classes fields, properties, and
* annotations among other things (property here means a bean standard property so a public getter
* and setter which conform to the bean standard), it may or may not be a complete record of the
* class<br/> This is primarily meant to provide easy access to information gathered via reflection
* and puts all that information in one convenient package which can be cached (but must be cached
* in a ClassLoader safe way)<br/>
* NOTE: It is important that you do not hold onto this object as
* it is meant to be cached in a way that it can be collected and will hold open the ClassLoader
* that the class came from<br/>
* Terminology:<br/>
* Complete field: the value of the field can be
* set (is not final or missing a setter) and retrieved (not missing a getter) <br/>
* Partial field:
* the value can either be set or retrieved but not both <br/>
* Settable: the value can be set on this field (final fields cannot be set) <br/>
* Gettable: the value can be retrieved from this field <br/>
* <br/>
* This object is immutable<br/>
*
* @author Aaron Zeckoski (azeckoski@gmail.com)
*/
public class ClassFields<T> {
/**
* Indicates the fields filtering to use when retrieving fields from types/objects: <br/>
* {@link #COMPLETE} (default)
*/
public static enum FieldsFilter {
/**
* all complete (read and write) fields,
* getters and setters included
*/
COMPLETE,
/**
* (default)
* all readable fields (may not be writeable),
* includes transient and virtual (getter only) fields
*/
READABLE,
/**
* all writeable fields (may not be readable),
* public and setters
*/
WRITEABLE,
/**
* all accessible serializable fields or getters (skips transients)
*/
SERIALIZABLE,
/**
* all accessible serializable fields (skips transient fields),
* no virtual fields (getters only)
*/
SERIALIZABLE_FIELDS,
/**
* all fields including ones that are virtual (read or write only)
*/
ALL
}
/**
* @param cp the {@link ClassProperty} object which represents a field
* @param filter (optional) indicates the fields to return the names for, can be null for defaults
* @return true if this field is in the given filter
*/
public boolean isFieldInFilter(ClassProperty cp, FieldsFilter filter) {
boolean in = false;
if (FieldsFilter.ALL.equals(filter)) {
in = true;
} else if (FieldsFilter.READABLE.equals(filter)) {
if ( isGettable(cp) ) {
in = true;
}
} else if (FieldsFilter.WRITEABLE.equals(filter)) {
if ( isSettable(cp) ) {
in = true;
}
} else if (FieldsFilter.SERIALIZABLE.equals(filter)) {
if ( isGettable(cp) && !cp.isTransient()) {
in = true;
}
} else if (FieldsFilter.SERIALIZABLE_FIELDS.equals(filter)) {
if (cp.isField() && !cp.isTransient()) {
in = true;
}
} else {
// return complete
if ( isComplete(cp) ) {
in = true;
}
}
return in;
}
/**
* Mode for finding the fields on classes/objects:<br/>
* {@link #HYBRID} (default)
*/
public static enum FieldFindMode {
/**
* (default) finds fields by matched public getters and setters first and then public fields
*/
HYBRID,
/**
* finds all fields which are accessible (public, protected, or private), ignores getters and setters
*/
FIELD,
/**
* find all matched getters and setters only, ignores fields
*/
PROPERTY,
/**
* finds all possible fields including matched getter/setter
*/
ALL
}
// PUBLIC access methods
/**
* @return the class that this {@link ClassFields} object represents the fields for
* @throws java.lang.Exception class loading exception,
* if the class this refers to has been garbage collected
*/
public Class<T> getFieldClass() {
return getStoredClass();
}
/**
* @return the field find mode being used (cannot be changed)
*/
public FieldFindMode getFieldFindMode() {
return fieldFindMode;
}
/**
* @return the list of fields we are explicitly skipping over
*/
public Set<String> getIgnoredFieldNames() {
return ignoredFieldNames;
}
/**
* @return the list of field names <br/> Only returns the complete
* fields, the partial ones (only settable or gettable) will not be returned
*/
public List<String> getFieldNames() {
return getFieldNames(null);
}
/**
* Get the field names but filter the fields to return
* @param filter (optional) indicates the fields to return the names for, can be null for defaults
* @return the list of field names
*/
public List<String> getFieldNames(FieldsFilter filter) {
List<String> names = new ArrayList<String>();
for (Entry<String, ClassProperty> entry : namesToProperties.getEntries()) {
String name = entry.getKey();
ClassProperty cp = entry.getValue();
if ( isFieldInFilter(cp, filter) ) {
names.add(name);
}
}
return names;
}
/**
* Get the types for fields in a class
* @return the map of fieldName -> field type
*/
public Map<String, Class<?>> getFieldTypes() {
return getFieldTypes(null);
}
/**
* Get the types for fields in a class but filter the fields to get the types for
* @param filter (optional) indicates the fields to return the types for, can be null for defaults
* @return the map of fieldName -> field type
*/
public Map<String, Class<?>> getFieldTypes(FieldsFilter filter) {
OrderedMap<String, Class<?>> fieldTypes = new ArrayOrderedMap<String, Class<?>>();
fieldTypes.setName(getStoredClass().getName());
for (Entry<String, ClassProperty> entry : namesToProperties.getEntries()) {
String name = entry.getKey();
ClassProperty cp = entry.getValue();
if ( isFieldInFilter(cp, filter) ) {
fieldTypes.put(name, cp.getType());
}
}
return fieldTypes;
}
/**
* @return the number of fields (only includes complete fields)
*/
public int size() {
return size(null);
}
/**
* @param filter (optional) indicates the fields to include in the count, can be null for defaults
* @return the number of fields
*/
public int size(FieldsFilter filter) {
int size;
if (FieldsFilter.ALL.equals(filter)) {
size = namesToProperties.size();
} else {
size = getFieldNames(filter).size();
}
return size;
}
/**
* @return true if this has no fields, false otherwise (only includes complete fields)
*/
public boolean isEmpty() {
return size() <= 0;
}
/**
* @param filter (optional) indicates the fields to include in the check, can be null for defaults
* @return true if this has no fields, false otherwise (only includes complete fields)
*/
public boolean isEmpty(FieldsFilter filter) {
return size(filter) <= 0;
}
/**
* Checks is this is a complete field
* @param name the fieldName
* @return true if this fieldName is valid, false otherwise
*/
public boolean isFieldNameValid(String name) {
return isFieldNameValid(name, null);
}
/**
* Checks if a field is valid for the given filter
*
* @param name the fieldName
* @param filter (optional) indicates the fields to include in the check, can be null for defaults
* @return true if this fieldName is valid, false otherwise
*/
public boolean isFieldNameValid(String name, FieldsFilter filter) {
boolean valid = namesToProperties.containsKey(name);
if (valid) {
// the field is real so check to see if it is in this filter
ClassProperty cp = getAnyPropertyOrFail(name);
if ( isFieldInFilter(cp, filter) ) {
valid = true;
} else {
valid = false;
}
}
return valid;
}
/**
* @param name the fieldName
* @return the type of this field
* @throws FieldnameNotFoundException if this fieldName is invalid
*/
public Class<?> getFieldType(String name) {
ClassProperty cp = getAnyPropertyOrFail(name);
Class<?> type = cp.getType();
return type;
}
// ANNOTATIONS
/**
* Get all annotations present on the represented class
*
* @return the set of all annotations on the class this refers to
*/
public Set<Annotation> getClassAnnotations() {
return new HashSet<Annotation>( classData.getAnnotations() );
}
/**
* Gets the annotation from the represented class if one exists of the type specified
*
* @param annotationType
* the annotation type to look for on this class
* @return the annotation if found OR null if none found
*/
@SuppressWarnings({ "unchecked", "hiding" })
public <T extends Annotation> T getClassAnnotation(Class<T> annotationType) {
if (annotationType == null) {
throw new IllegalArgumentException("annotationType must not be null");
}
T annote = null;
List<Annotation> annotations = classData.getAnnotations();
for (Annotation annotation : annotations) {
if (annotationType.equals(annotation.annotationType())) {
annote = (T) annotation;
break;
}
}
return annote;
}
/**
* Get all annotations on the field in the represented class
*
* @param name the fieldName
* @return the set of all annotations on this field
* @throws FieldnameNotFoundException if this fieldName is invalid
*/
public Set<Annotation> getFieldAnnotations(String name) {
ClassProperty cp = getAnyPropertyOrFail(name);
Collection<Annotation> annotes = cp.getAnnotationsCollection();
Set<Annotation> s = new HashSet<Annotation>(annotes);
return s;
}
/**
* @param annotationType
* the annotation type to look for on this field
* @param name the fieldName
* @return the annotation if found OR null if none found
* @throws FieldnameNotFoundException if this fieldName is invalid
*/
@SuppressWarnings("hiding")
public <T extends Annotation> T getFieldAnnotation(Class<T> annotationType, String name) {
if (annotationType == null) {
throw new IllegalArgumentException("annotationType must not be null");
}
ClassProperty cp = getAnyPropertyOrFail(name);
T a = cp.getAnnotation(annotationType);
return a;
}
/**
* Will find the first field which has this annotation type
*
* @param annotationType
* the annotation type to look for on this field
* @return the name of the field which has this annotation OR null if none found
*/
public String getFieldNameByAnnotation(Class<? extends Annotation> annotationType) {
if (annotationType == null) {
throw new IllegalArgumentException("annotationType must not be null");
}
String fieldName = null;
Collection<ClassProperty> cps = namesToProperties.values();
for (ClassProperty classProperty : cps) {
if (classProperty.getAnnotation(annotationType) != null) {
fieldName = classProperty.getFieldName();
break;
}
}
return fieldName;
}
/**
* Finds the names of all the fields with the given annotation type
* @param annotationType
* the annotation type to look for on this field
* @return the list of all field names sorted OR empty if none found
*/
public List<String> getFieldNamesWithAnnotation(Class<? extends Annotation> annotationType) {
if (annotationType == null) {
throw new IllegalArgumentException("annotationType must not be null");
}
HashSet<String> fieldNames = new HashSet<String>();
Collection<ClassProperty> cps = namesToProperties.values();
for (ClassProperty classProperty : cps) {
if (classProperty.getAnnotation(annotationType) != null) {
fieldNames.add( classProperty.getFieldName() );
}
}
ArrayList<String> l = new ArrayList<String>(fieldNames);
Collections.sort(l);
return l;
}
// SPECIAL methods
/**
* SPECIAL METHOD: accesses the internal data<br/>
* Returns a field property object for any field (not limited to only complete fields)
*
* @param name the fieldName
* @return the Property object which holds information about a field
* @throws FieldnameNotFoundException if this fieldName is invalid
*/
public ClassProperty getClassProperty(String name) {
ClassProperty cp = getAnyPropertyOrFail(name);
return cp;
}
/**
* SPECIAL METHOD: accesses the internal data<br/>
* Gets all the cached class properties objects related to this class<br/>
* WARNING: Special method which allows access to the internal properties storage structure,
* this should really only be used for filtering results in a way that is not supported by the
* standard class fields methods
*
* @return the complete set of field properties objects for this class
*/
public Map<String, ClassProperty> getAllClassProperties() {
return this.namesToProperties;
}
/**
* SPECIAL METHOD: accesses the internal data<br/>
* Gets the internal cache object for the class which holds all fields, methods, constructors, and annotations
* @return the class data cache object
*/
public ClassData<T> getClassData() {
return this.classData;
}
// VARIABLES
public static final String FIELD_CLASS = "class";
public static final String METHOD_GET_CLASS = "getClass";
private static final String PREFIX_IS = "is";
private static final String PREFIX_GET = "get";
private static final String PREFIX_SET = "set";
private final FieldFindMode fieldFindMode;
/**
* if true then include the "class" field as if it were a regular field
*/
private boolean includeClassField = false;
/**
* if true then include static fields, otherwise skip them (default is to skip)
*/
private boolean includeStaticFields = false;
/**
* includes all the field names which should be ignored for reflection
*/
private final Set<String> ignoredFieldNames = new HashSet<String>();
/**
* includes all the field names which should be marked as transient
*/
private final Set<String> transientFieldNames = new HashSet<String>();
// WARNING: all these things can hold open a ClassLoader
private final ClassData<T> classData;
private final OrderedMap<String, ClassProperty> namesToProperties; // this contains all properties data (includes partials)
// PUBLIC constructors
/**
* Constructor for when you have everything already in nice arrays, the getter and setter method
* arrays MUST be the same length AND the same order (i.e. getter[0] and setter[0] must refer to
* the same property), no arguments can be null but arrays can be empty, null values in arrays
* will be ignored<br/>
* <b>NOTE:</b> validation of the getters and setters is somewhat costly
* so you may want to cache the constructed object
*
* @param fieldClass
* this is the class whose fields are represented
* @param getterMethods
* array of getter methods for properties
* @param setterMethods
* array of setter methods for properties
* @param publicFields
* array of public fields
*/
public ClassFields(Class<T> fieldClass, Method[] getterMethods, Method[] setterMethods, Field[] publicFields) {
if (fieldClass == null || getterMethods == null || setterMethods == null
|| publicFields == null) {
throw new IllegalArgumentException("None of the params can be null");
}
if (getterMethods.length != setterMethods.length) {
throw new IllegalArgumentException("Getter and setter methods must be the same length");
}
// set the field class
classData = new ClassData<T>(fieldClass);
fieldFindMode = FieldFindMode.HYBRID;
// set the properties
namesToProperties = new ArrayOrderedMap<String, ClassProperty>(getterMethods.length
+ publicFields.length);
for (int i = 0; i < getterMethods.length; i++) {
if (getterMethods[i] != null && setterMethods[i] != null) {
String fieldName = checkPropertyMethods(getterMethods[i], setterMethods[i]);
if (!namesToProperties.containsKey(fieldName)) {
ClassProperty p = new ClassProperty(fieldName, getterMethods[i],
setterMethods[i]);
namesToProperties.put(fieldName, p);
}
}
}
populateFields(publicFields);
populateAnnotationsFields();
}
/**
* Constructor for when you have descriptors and fields in arrays (if you have descriptors it is
* easy to get the fields from the class using {@link Class#getFields()}), no arguments can be
* null but the arrays can be empty
*
* @param fieldClass
* this is the class whose fields are represented
* @param descriptors
* an array of descriptors, you can get these from
* {@link BeanInfo#getPropertyDescriptors()} and you can get the {@link BeanInfo}
* from {@link Introspector#getBeanInfo(Class)}
* @param publicFields
* array of public fields
*/
public ClassFields(Class<T> fieldClass, PropertyDescriptor[] descriptors, Field[] publicFields) {
if (fieldClass == null || descriptors == null || publicFields == null) {
throw new IllegalArgumentException("None of the params can be null");
}
classData = new ClassData<T>(fieldClass);
fieldFindMode = FieldFindMode.HYBRID;
namesToProperties = new ArrayOrderedMap<String, ClassProperty>(descriptors.length + publicFields.length);
populateProperties(descriptors);
populateFields(publicFields);
populateAnnotationsFields();
}
/**
* Constructor to use when you know nothing about the class at all, this will use very
* straightforward walking over the PUBLIC methods and fields on the class and will extract the field
* information, this uses the default field finding mode of {@link FieldFindMode#HYBRID}<br/>
* <b>NOTE:</b> This is fairly expensive so it is recommended that you cache the constructed object
* by using {@link FieldUtils}
*
* @param fieldClass
* this is the class whose fields are represented
*/
public ClassFields(Class<T> fieldClass) {
this(fieldClass, FieldFindMode.HYBRID, false, false);
}
/**
* Constructor to use when you know nothing about the class at all, this will use very
* straightforward walking over the methods and fields on the class and will extract the field
* information according to the field finding mode<br/>
* <b>NOTE:</b> This is fairly expensive so it is recommended that you cache the constructed object
* by using {@link FieldUtils}
*
* @param fieldClass this is the class whose fields are represented
* @param findMode the search mode for fields on the classes, allows the developer to control the way the
* fields are found for later usage<br/>
* HYBRID (default): finds fields by matched public getters and setters first and then public fields <br/>
* FIELD: finds all fields which are accessible (public, protected, or private), ignores getters and setters <br/>
* PROPERTY: find all matched getters and setters only, ignores fields <br/>
*/
public ClassFields(Class<T> fieldClass, FieldFindMode findMode) {
this(fieldClass, findMode, false, false);
}
/**
* Constructor to use when you know nothing about the class at all, this will use the java
* introspector as long as the param is true, otherwise it will simply call over to
* {@link ClassFields#ClassFields(Class)} and use very
* straightforward walking over the methods and fields on the class to extract the field
* information according to the field finding mode<br/>
* <b>NOTE:</b> This is fairly expensive so it is recommended that you cache the constructed object
* by using {@link FieldUtils}
*
* @param fieldClass this is the class whose fields are represented
* @param findMode the search mode for fields on the classes, allows the developer to control the way the
* fields are found for later usage<br/>
* HYBRID (default): finds fields by matched public getters and setters first and then public fields <br/>
* FIELD: finds all fields which are accessible (public, protected, or private), ignores getters and setters <br/>
* PROPERTY: find all matched getters and setters only, ignores fields <br/>
* @param useIntrospector
* if this is true then the {@link Introspector} is used, this is generally the
* slowest way to get information about a class but some people prefer it, if false
* then the internal methods are used
* @param includeClassFields if true then the "class" field (the result of getClass()) is included as a read only field
*/
public ClassFields(Class<T> fieldClass, FieldFindMode findMode, boolean useIntrospector, boolean includeClassFields) {
// set the field class
if (fieldClass == null) {
throw new IllegalArgumentException("field class cannot be null");
}
classData = new ClassData<T>(fieldClass);
fieldFindMode = findMode;
includeClassField = includeClassFields;
namesToProperties = new ArrayOrderedMap<String, ClassProperty>();
// check for reflect annotations on the class
List<Annotation> annotations = classData.getAnnotations();
for (Annotation annotation : annotations) {
// note that we compare the simple name to avoid issues with cross classloader types
if (ReflectIncludeStaticFields.class.getSimpleName().equals(annotation.annotationType().getSimpleName())) {
includeStaticFields = true;
} else if (ReflectIgnoreClassFields.class.getSimpleName().equals(annotation.annotationType().getSimpleName())) {
// this does not work if the classloader of the annotation is not our classloader
String[] ignore = new String[0];
if (ReflectIgnoreClassFields.class.equals(annotation.annotationType())) {
ignore = ((ReflectIgnoreClassFields)annotation).value();
} else {
// get the value out by reflection on the annotation
ClassData<?> cd = ClassDataCacher.getInstance().getClassData(annotation);
List<Method> methods = cd.getMethods();
for (Method method : methods) {
if (method.getName().equals("value")) {
try {
@SuppressWarnings("RedundantArrayCreation") Object o = method.invoke(annotation, new Object[] {});
ignore = (String[]) o;
} catch (Exception e) {
throw new RuntimeException("Annotation of the same name has invalid value() method ("+method+") and is not of the right type: " + ReflectIgnoreClassFields.class);
}
break;
}
}
}
Collections.addAll(ignoredFieldNames, ignore);
} else if (ReflectTransientClassFields.class.getSimpleName().equals(annotation.annotationType().getSimpleName())) {
// this does not work if the classloader of the annotation is not our classloader
String[] transients = new String[0];
if (ReflectTransientClassFields.class.equals(annotation.annotationType())) {
transients = ((ReflectTransientClassFields)annotation).value();
} else {
// get the value out by reflection on the annotation
ClassData<?> cd = ClassDataCacher.getInstance().getClassData(annotation);
List<Method> methods = cd.getMethods();
for (Method method : methods) {
if (method.getName().equals("value")) {
try {
@SuppressWarnings("RedundantArrayCreation") Object o = method.invoke(annotation, new Object[] {});
transients = (String[]) o;
} catch (Exception e) {
throw new RuntimeException("Annotation of the same name has invalid value() method ("+method+") and is not of the right type: " + ReflectTransientClassFields.class);
}
break;
}
}
}
Collections.addAll(transientFieldNames, transients);
}
}
// populate the internal storage with the list of properties
if (useIntrospector) {
// use the java Introspector
BeanInfo bi;
try {
bi = Introspector.getBeanInfo(fieldClass);
PropertyDescriptor[] descriptors = bi.getPropertyDescriptors();
if (descriptors != null) {
populateProperties(descriptors);
// fields
populateFields(false);
} else {
useIntrospector = false;
}
} catch (IntrospectionException e) {
// no descriptors so fail over to using the internal methods
useIntrospector = false;
}
}
if (!useIntrospector) {
// construct using pure reflection
if (FieldFindMode.HYBRID.equals(findMode) || FieldFindMode.PROPERTY.equals(findMode) || FieldFindMode.ALL.equals(findMode)) {
List<ClassProperty> properties = findProperties(true);
for (ClassProperty property : properties) {
String fieldName = property.getFieldName();
namesToProperties.put(fieldName, property);
}
}
if (FieldFindMode.HYBRID.equals(findMode) || FieldFindMode.FIELD.equals(findMode) || FieldFindMode.ALL.equals(findMode)) {
if (FieldFindMode.FIELD.equals(findMode) || FieldFindMode.ALL.equals(findMode)) {
populateFields(true);
} else {
populateFields(false);
}
}
}
populateAnnotationsFields();
}
// PRIVATE analyzer methods
/**
* @param name
* @return
*/
private ClassProperty getAnyPropertyOrFail(String name) {
ClassProperty cp = namesToProperties.get(name);
if (cp == null) {
throw new FieldnameNotFoundException(name);
}
return cp;
}
private Class<T> getStoredClass() {
return classData.getType();
}
/**
* Determines if a {@link ClassProperty} is complete based on the {@link FieldFindMode} setting
* @param cp a class property setting
* @return true if it is complete, false otherwise
*/
private boolean isComplete(ClassProperty cp) {
boolean complete = false;
if (FieldFindMode.FIELD.equals(fieldFindMode)) {
if (cp.isField()) {
complete = true;
}
} else if (FieldFindMode.PROPERTY.equals(fieldFindMode)) {
if (cp.isProperty()) {
complete = true;
}
} else {
// default
if (cp.isComplete()) {
complete = true;
}
}
return complete;
}
/**
* Determines if a {@link ClassProperty} is gettable based on the {@link FieldFindMode} setting
* @param cp a class property setting
* @return true if it is gettable, false otherwise
*/
private boolean isGettable(ClassProperty cp) {
boolean gettable = false;
if (FieldFindMode.FIELD.equals(fieldFindMode)) {
if (cp.isField() && cp.isGettable()) {
gettable = true;
}
} else if (FieldFindMode.PROPERTY.equals(fieldFindMode)) {
if (cp.isProperty() && cp.isGettable()) {
gettable = true;
}
} else {
// default
if (cp.isPublicGettable()) {
gettable = true;
}
}
return gettable;
}
/**
* Determines if a {@link ClassProperty} is settable based on the {@link FieldFindMode} setting
* @param cp a class property setting
* @return true if it is settable, false otherwise
*/
private boolean isSettable(ClassProperty cp) {
boolean settable = false;
if (FieldFindMode.FIELD.equals(fieldFindMode)) {
if (cp.isField() && cp.isSettable()) {
settable = true;
}
} else if (FieldFindMode.PROPERTY.equals(fieldFindMode)) {
if (cp.isProperty() && cp.isSettable()) {
settable = true;
}
} else {
// default
if (cp.isPublicSettable()) {
settable = true;
}
}
return settable;
}
/**
* Populate the internal storage from an array of descriptors
*
* @param descriptors
*/
private void populateProperties(PropertyDescriptor[] descriptors) {
for (PropertyDescriptor pd : descriptors) {
String fieldName = pd.getName();
if (!namesToProperties.containsKey(fieldName)) {
Method getter = pd.getReadMethod();
Method setter = pd.getWriteMethod();
if (getter != null && setter != null) {
ClassProperty p = null;
if (pd instanceof IndexedPropertyDescriptor) {
IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
// Class<?> iType = ipd.getIndexedPropertyType();
Method iGetter = ipd.getIndexedReadMethod();
Method iSetter = ipd.getIndexedWriteMethod();
if (iGetter != null && iSetter != null) {
p = new ClassProperty.IndexedProperty(fieldName, getter, setter, iGetter, iSetter);
}
}
if (p == null) {
p = new ClassProperty(fieldName, getter, setter);
}
namesToProperties.put(fieldName, p);
}
}
}
}
/**
* Checks a getter and setter pair and returns the proper property name
*
* @param getter
* @param setter
* @return the property name for this getter and setter pair
*/
private String checkPropertyMethods(Method getter, Method setter) {
String name = null;
String gName = getter.getName();
Class<?> gType = null;
Class<?>[] paramTypes = getter.getParameterTypes();
if (paramTypes.length == 0) {
if (isGetClassMethod(getter)) {
throw new GetClassMethodException();
} else if (gName.startsWith(PREFIX_GET) || gName.startsWith(PREFIX_IS)) {
gType = getter.getReturnType();
if (gType != null) {
name = makeFieldNameFromMethod(gName);
}
}
}
if (name != null) {
boolean validSetter = false;
String sName = setter.getName();
Class<?> sType;
if (sName.startsWith(PREFIX_SET)) {
Class<?>[] sParamTypes = setter.getParameterTypes();
if (sParamTypes.length == 1) {
sType = sParamTypes[0];
if (sType == null || !sType.isAssignableFrom(gType)) {
throw new IllegalArgumentException("setter (" + sName + ") type (" + sType
+ ") and " + "getter (" + gName + ") type (" + gType
+ ") do not match, they MUST match");
} else {
validSetter = true;
}
}
}
if (!validSetter) {
throw new IllegalArgumentException("setter (" + sName
+ ") does not appear to be a valid setter method "
+ "(single param, name starts with set)");
}
} else {
throw new IllegalArgumentException("getter (" + gName
+ ") does not appear to be a valid getter method "
+ "(0 params, name starts with get or is)");
}
return name;
}
/**
* Simple manual method for walking over a class and finding the bean properties, generally used
* because the sun Introspector is so slow, note that this will also return all the non-matching
* getters or setters if requested
*
* @param includePartial
* if true then includes getters and setters that do not match, if false then ONLY
* include the getters and setters which match exactly
* @return a list Property objects
*/
@SuppressWarnings("SameParameterValue")
private List<ClassProperty> findProperties(boolean includePartial) {
// find the property methods from all public methods
Map<String, ClassProperty> propertyMap = new ArrayOrderedMap<String, ClassProperty>();
for (Method method : classData.getPublicMethods()) {
Class<?>[] paramTypes = method.getParameterTypes();
String name = method.getName();
if (! includeStaticFields && isStatic(method)) {
continue; // skip statics
} else if (isGetClassMethod(method) && ! includeClassField) {
continue; // skip class field
} else if (paramTypes.length == 0) {
// no params
if (name.startsWith(PREFIX_GET) || name.startsWith(PREFIX_IS)) {
// getter
Class<?> returnType = method.getReturnType();
if (returnType != null) {
try {
String fieldName = makeFieldNameFromMethod(method.getName());
ClassProperty p;
if (propertyMap.containsKey(fieldName)) {
p = propertyMap.get(fieldName);
p.setGetter(method);
} else {
p = new ClassProperty(fieldName, method, null);
propertyMap.put(fieldName, p);
}
} catch (Exception e) {
// nothing to do here but move on
}
}
}
} else if (paramTypes.length == 1) {
if (name.startsWith(PREFIX_SET)) {
// setter (one param)
String fieldName = makeFieldNameFromMethod(method.getName());
ClassProperty p;
if (propertyMap.containsKey(fieldName)) {
p = propertyMap.get(fieldName);
p.setSetter(method);
} else {
p = new ClassProperty(fieldName, null, method);
propertyMap.put(fieldName, p);
}
}
}
}
List<ClassProperty> properties = new ArrayList<ClassProperty>();
for (Entry<String, ClassProperty> entry : propertyMap.entrySet()) {
ClassProperty p = entry.getValue();
if (p != null) {
if (! includeStaticFields && p.isStatic()) {
continue; // skip any props with static fields
}
if (ignoredFieldNames.contains(p.getFieldName())) {
continue; // skip ignored names
}
if (includePartial) {
// put all of them in
properties.add(p);
} else {
// filter down to only the one which are complete
if (p.isGettable() && p.isSettable()) {
properties.add(p);
}
}
}
}
return properties;
}
/**
* Populates the fields from the class data object
* @param includeNonPublic if true then all fields will be used, if false then only public fields
*/
private void populateFields(boolean includeNonPublic) {
// set the fields
List<Field> fields;
if (includeNonPublic) {
fields = classData.getFields();
} else {
fields = classData.getPublicFields();
}
for (Field field : fields) {
if (! includeStaticFields && isStatic(field)) {
continue; // skip statics
}
String fieldName = field.getName();
if (ignoredFieldNames.contains(fieldName)) {
continue; // skip ignored names
}
if (! namesToProperties.containsKey(fieldName)) {
ClassProperty p = new ClassProperty(fieldName, field);
namesToProperties.put(fieldName, p);
}
}
}
/**
* @param member any class member
* @return true if static, false otherwise
*/
protected boolean isStatic(Member member) {
if ( Modifier.isStatic(member.getModifiers()) ) {
return true;
}
return false;
}
/**
* @param publicFields
* takes an array of fields and populates the internal storage mechanisms
*/
private void populateFields(Field[] publicFields) {
// set the fields
for (Field publicField : publicFields) {
if (publicField != null) {
String fieldName = publicField.getName();
if (!namesToProperties.containsKey(fieldName)) {
ClassProperty p = new ClassProperty(fieldName, publicField);
namesToProperties.put(fieldName, p);
}
}
}
}
/**
* We need to find all the class, method, and field annotations and not just the ones on public
* methods so we are going through everything on this class and all the super classes<br/>
* WARNING: this is NOT cheap and the results need to be cached badly<br/>
* NOTE: This also populates all the fields in every property and checks and removes
* all statics or excluded fields if any slipped through the previous processes
*/
private void populateAnnotationsFields() {
// get the annotations from all class methods
for (Method method : classData.getMethods()) {
int paramCount = method.getParameterTypes().length;
// only include annotations on methods which *might* be fields (no args, 1 arg, or 2 args for the special ones)
if (paramCount <= 2) {
try {
String name = makeFieldNameFromMethod(method.getName());
ClassProperty cp = getAnyPropertyOrFail(name);
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation != null) cp.addAnnotation(annotation);
}
} catch (FieldnameNotFoundException e) {
// nothing to do but keep going
}
}
}
// get the annotations from all class fields
for (Field field : classData.getFields()) {
try {
String fieldName = field.getName();
if (! includeStaticFields && isStatic(field)) {
namesToProperties.remove(fieldName); // as a final check, take this out if it is in there
continue; // skip statics
}
if (ignoredFieldNames.contains(fieldName)) {
namesToProperties.remove(fieldName); // as a final check, take this out if it is in there
continue; // skip ignored names
}
ClassProperty cp = getAnyPropertyOrFail(fieldName);
if (cp.getField() == null) {
cp.setField(field); // ensure there is a field set for all methods
}
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation != null) cp.addAnnotation(annotation);
}
} catch (FieldnameNotFoundException e) {
// nothing to do but keep going
}
}
// now mark the transient fields as such
for (String fieldName : transientFieldNames) {
try {
ClassProperty cp = getAnyPropertyOrFail(fieldName);
cp.transientField = true;
} catch (FieldnameNotFoundException e) {
// nothing to do but keep going
}
}
}
@Override
public String toString() {
return "(" + getStoredClass().getName() + "):mode="+fieldFindMode.name()+":" + getFieldNames(FieldsFilter.ALL);
}
// STATIC methods
/**
* @param methodName
* a getter or setter style method name (e.g. getThing, setStuff, isType)
* @return the fieldName equivalent (thing, stuff, type)
*/
public static String makeFieldNameFromMethod(String methodName) {
String name;
if (methodName.startsWith(PREFIX_IS)) {
name = unCapitalize(methodName.substring(2));
} else if (methodName.startsWith(PREFIX_GET) || methodName.startsWith(PREFIX_SET)) {
// set or get
name = unCapitalize(methodName.substring(3));
} else {
name = methodName;
}
return name;
}
/**
* Capitalize a string
*
* @param input
* any string
* @return the string capitalized (e.g. myString -> MyString)
*/
public static String capitalize(String input) {
String cap;
if (input.length() == 0) {
cap = "";
} else {
char first = Character.toUpperCase(input.charAt(0));
if (input.length() == 1) {
cap = first + "";
} else {
cap = first + input.substring(1);
}
}
return cap;
}
/**
* undo string capitalization
*
* @param input
* any string
* @return the string uncapitalized (e.g. MyString -> myString)
*/
public static String unCapitalize(String input) {
String cap;
if (input.length() == 0) {
cap = "";
} else {
char first = Character.toLowerCase(input.charAt(0));
if (input.length() == 1) {
cap = first + "";
} else {
cap = first + input.substring(1);
}
}
return cap;
}
/**
* @param method
* any method
* @return true if this is the getClass() method for standard objects
*/
public static boolean isGetClassMethod(Method method) {
boolean gc = false;
if (method != null) {
if (METHOD_GET_CLASS.equals(method.getName())) {
gc = true;
}
}
return gc;
}
// INNER classes
/**
* Indicates that this is the getter or setter for the class type and should be ignored
*/
public static class GetClassMethodException extends RuntimeException {
public GetClassMethodException() {
super();
}
}
}