AbstractClassInfoStrategy.java
/**
*
*/
package uk.co.jemos.podam.api;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.co.jemos.podam.common.PodamExclude;
/**
* Default abstract implementation of a {@link ClassInfoStrategy}
* <p>
* This default implementation is based on field introspection.
* </p>
*
* @author daivanov
*
* @since 5.1.0
*
*/
public abstract class AbstractClassInfoStrategy implements ClassInfoStrategy,
ClassAttributeApprover {
// ------------------->> Constants
private final Pattern GETTER_PATTERN = getGetterPattern();
private final Pattern SETTER_PATTERN = getSetterPattern();
// ------------------->> Instance / Static variables
/** The application logger. */
private static final Logger LOG = LoggerFactory.getLogger(AbstractClassInfoStrategy.class);
/**
* Set of annotations, which mark fields to be skipped from populating.
*/
private final Set<Class<? extends Annotation>> excludedAnnotations =
new HashSet<Class<? extends Annotation>>();
/**
* Set of fields, which mark fields to be skipped from populating.
*/
private Map<Class<?>, Set<String>> excludedFields
= new HashMap<Class<?>, Set<String>>();
/**
* Set of extra methods to execute.
* @since 5.3.0
**/
private final Map<Class<?>, List<Method>> extraMethods = new HashMap<Class<?>, List<Method>>();
// ------------------->> Constructors
// ------------------->> Public methods
/**
* Adds the specified {@link Annotation} to set of excluded annotations,
* if it is not already present.
*
* @param annotation
* the annotation to use as an exclusion mark
* @return itself
*/
public AbstractClassInfoStrategy addExcludedAnnotation(
final Class<? extends Annotation> annotation) {
excludedAnnotations.add(annotation);
return this;
}
/**
* It adds an extra method to execute
* @param pojoClass The pojo class where to execute the method
* @param methodName name to be scheduled for execution
* @param methodArgs list of method arguments
* @return this object
* @throws SecurityException If a security exception occurred while retrieving the method
* @throws NoSuchMethodException If pojoClass doesn't declare the required method
* @since 5.3.0
*/
public AbstractClassInfoStrategy addExtraMethod(
Class<?> pojoClass, String methodName, Class<?> ... methodArgs)
throws NoSuchMethodException, SecurityException {
Method method = pojoClass.getMethod(methodName, methodArgs);
List<Method> methods = extraMethods.get(pojoClass);
if (methods == null) {
methods = new ArrayList<Method>();
extraMethods.put(pojoClass, methods);
}
methods.add(method);
return this;
}
/**
* Removes the specified {@link Annotation} from set of excluded annotations.
*
* @param annotation
* the annotation used as an exclusion mark
* @return itself
*/
public AbstractClassInfoStrategy removeExcludedAnnotation(
final Class<? extends Annotation> annotation) {
excludedAnnotations.remove(annotation);
return this;
}
/**
* Adds the specified field to set of excluded fields,
* if it is not already present.
*
* @param pojoClass
* a class for which fields should be skipped
* @param fieldName
* the field name to use as an exclusion mark
* @return itself
*/
public AbstractClassInfoStrategy addExcludedField(
final Class<?> pojoClass, final String fieldName) {
Set<String> fields = excludedFields.get(pojoClass);
if (fields == null) {
fields = new HashSet<String>();
excludedFields.put(pojoClass, fields);
}
fields.add(fieldName);
return this;
}
/**
* Removes the field name from set of excluded fields.
*
* @param pojoClass
* a class for which fields should be skipped
* @param fieldName
* the field name used as an exlusion mark
* @return itself
*/
public AbstractClassInfoStrategy removeExcludedField(
final Class<?> pojoClass, final String fieldName) {
Set<String> fields = excludedFields.get(pojoClass);
if (fields != null) {
fields.remove(fieldName);
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public boolean approve(ClassAttribute attribute) {
/* skip setters having more than one parameter,
* when there is more than one setter for a field */
if (attribute.getRawSetters().size() > 1) {
for (Method setter : attribute.getRawSetters()) {
if (setter.getParameterTypes().length > 1) {
return false;
}
}
}
return (attribute.getAttribute() != null);
}
// ------------------->> Getters / Setters
/**
* {@inheritDoc}
*/
@Override
public Set<Class<? extends Annotation>> getExcludedAnnotations() {
return excludedAnnotations;
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getExcludedFields(final Class<?> pojoClass) {
return excludedFields.get(pojoClass);
}
/**
* {@inheritDoc}
*/
@Override
public ClassInfo getClassInfo(Class<?> pojoClass) {
Set<String> excludedAttributes = excludedFields.get(pojoClass);
if (null == excludedAttributes) {
excludedAttributes = Collections.emptySet();
}
List<Method> localExtraMethods = extraMethods.get(pojoClass);
if (null == localExtraMethods) {
localExtraMethods = Collections.emptyList();
}
return getClassInfo(pojoClass,
excludedAnnotations, excludedAttributes, this, localExtraMethods);
}
@Override
public ClassAttributeApprover getClassAttributeApprover(Class<?> pojoClass) {
return this;
}
@Override
public Collection<Method> getExtraMethods(Class<?> pojoClass) {
return extraMethods.get(pojoClass);
}
/**
* It returns a {@link ClassInfo} object for the given class
*
* @param clazz
* The class to retrieve info from
* @param excludeFieldAnnotations
* the fields marked with any of these annotations will not be
* included in the class info
* @param excludedFields
* the fields matching the given names will not be included in the class info
* @param attributeApprover
* a {@link ClassAttributeApprover} implementation,
* which defines which attributes to skip and which to process
* @param extraMethods
* extra methods to call after object initialization
* @return a {@link ClassInfo} object for the given class
*/
public ClassInfo getClassInfo(Class<?> clazz,
Set<Class<? extends Annotation>> excludeFieldAnnotations,
Set<String> excludedFields,
ClassAttributeApprover attributeApprover,
Collection<Method> extraMethods) {
if (null == attributeApprover) {
attributeApprover = DefaultClassInfoStrategy.getInstance().getClassAttributeApprover(clazz);
}
Map<String, ClassAttribute> attributeMap = new TreeMap<String, ClassAttribute>();
findPojoAttributes(clazz, attributeMap, excludeFieldAnnotations, excludedFields);
/* Approve all found attributes */
Collection<ClassAttribute> attributes = new ArrayList<ClassAttribute>(attributeMap.values());
Iterator<ClassAttribute> iter = attributes.iterator();
main : while(iter.hasNext()) {
ClassAttribute attribute = iter.next();
Field field = attribute.getAttribute();
if (excludedFields.contains(attribute.getName()) ||
(field != null && containsAnyAnnotation(field, excludeFieldAnnotations))) {
iter.remove();
continue;
}
for (Method classGetter : attribute.getRawGetters()) {
if (containsAnyAnnotation(classGetter, excludeFieldAnnotations)) {
iter.remove();
continue main;
}
}
for (Method classSetter : attribute.getRawSetters()) {
if (containsAnyAnnotation(classSetter, excludeFieldAnnotations)) {
iter.remove();
continue main;
}
}
if (!attributeApprover.approve(attribute)) {
iter.remove();
}
}
return new ClassInfo(clazz, attributes, extraMethods);
}
// ------------------->> Private methods
/**
* Checks if the given method has any one of the annotations
*
* @param method
* the method to check for
* @param annotations
* the set of annotations to look for in the field
* @return true if the field is marked with any of the given annotations
*/
private boolean containsAnyAnnotation(Method method,
Set<Class<? extends Annotation>> annotations) {
for (Class<? extends Annotation> annotation : annotations) {
if (method.getAnnotation(annotation) != null) {
return true;
}
}
return false;
}
/**
* Checks if the given field has any one of the annotations
*
* @param field
* the field to check for
* @param annotations
* the set of annotations to look for in the field
* @return true if the field is marked with any of the given annotations
*/
private boolean containsAnyAnnotation(Field field,
Set<Class<? extends Annotation>> annotations) {
for (Class<? extends Annotation> annotation : annotations) {
if (field.getAnnotation(annotation) != null) {
return true;
}
}
return false;
}
/**
* Given a class and a set of class declared fields it returns a map of
* setters, getters and fields defined for this class
*
* @param clazz
* The class to analyze for setters
* @param attributeMap
* The {@link Map} which will be filled with class' attributes
* @param excludeAnnotations
* The {@link Set} containing annotations marking fields to be excluded
* @param excludedFields
* The {@link Set} containing field names to be excluded
*/
protected void findPojoAttributes(Class<?> clazz,
Map<String, ClassAttribute> attributeMap,
Set<Class<? extends Annotation>> excludeAnnotations,
Set<String> excludedFields) {
if (excludeAnnotations == null) {
excludeAnnotations = new HashSet<Class<? extends Annotation>>();
}
excludeAnnotations.add(PodamExclude.class);
Class<?> workClass = clazz;
while (!Object.class.equals(workClass)) {
Method[] declaredMethods = workClass.getDeclaredMethods();
Field[] declaredFields = workClass.getDeclaredFields();
for (Field field : declaredFields) {
int modifiers = field.getModifiers();
if (!Modifier.isStatic(modifiers)) {
String attributeName = field.getName();
ClassAttribute attribute = attributeMap.get(attributeName);
if (attribute != null) {
/* In case we have hidden fields, we probably want the
* latest one, but there could be corner cases */
if (attribute.getAttribute() == null) {
attribute.setAttribute(field);
}
} else {
attribute = new ClassAttribute(field.getName(),
field, Collections.<Method>emptySet(), Collections.<Method>emptySet());
attributeMap.put(field.getName(), attribute);
}
}
}
for (Method method : declaredMethods) {
/*
* Bridge methods are automatically generated by compiler to
* deal with type erasure and they are not type safe. That why
* they should be ignored
*/
if (!method.isBridge() && !Modifier.isNative(method.getModifiers())) {
Pattern pattern;
if (method.getParameterTypes().length == 0
&& !method.getReturnType().equals(void.class)) {
pattern = GETTER_PATTERN;
} else if (method.getParameterTypes().length > 0
&& (method.getReturnType().equals(void.class)
|| method.getReturnType().isAssignableFrom(workClass))) {
pattern = SETTER_PATTERN;
} else {
continue;
}
String methodName = method.getName();
String attributeName = extractFieldNameFromMethod(methodName,
pattern);
if (!attributeName.equals(methodName)) {
if (!attributeName.isEmpty()) {
ClassAttribute attribute = attributeMap.get(attributeName);
if (attribute == null) {
attribute = new ClassAttribute(attributeName, null,
Collections.<Method>emptySet(),
Collections.<Method>emptySet());
attributeMap.put(attributeName, attribute);
}
Set<Method> accessors;
if (pattern == GETTER_PATTERN) {
accessors = attribute.getRawGetters();
} else {
accessors = attribute.getRawSetters();
}
accessors.add(method);
} else {
LOG.debug("Encountered accessor {}. This will be ignored.", method);
}
}
}
}
workClass = workClass.getSuperclass();
}
}
/**
* Given a accessor's name, it extracts the field name, according to
* JavaBean standards
* <p>
* This method, given a accessor method's name, it returns the corresponding
* attribute name. For example: given setIntField the method would return
* intField. given getIntField the method would return intField; given
* isBoolField the method would return boolField.The correctness of the
* return value depends on the adherence to
* JavaBean standards.
* </p>
*
* @param methodName
* The accessor method from which the field name is required
* @param pattern
* The pattern to strip from the method name
* @return The field name corresponding to the setter
*/
protected String extractFieldNameFromMethod(String methodName, Pattern pattern) {
String candidateField = pattern.matcher(methodName).replaceFirst("");
if (!candidateField.isEmpty() && !candidateField.equals(methodName)) {
String candidateFieldEnding = candidateField.substring(1);
if ((candidateFieldEnding.isEmpty()
|| !candidateFieldEnding.toUpperCase().equals(candidateFieldEnding)
|| candidateFieldEnding.toLowerCase().equals(candidateFieldEnding.toUpperCase()))) {
candidateField = Character.toLowerCase(candidateField.charAt(0))
+ candidateFieldEnding;
}
}
return candidateField;
}
/**
* Defines a regular expression for a getter's name
*
* @return a compiled pattern for the getter's name
*/
protected Pattern getGetterPattern() {
return Pattern.compile("^(get|is)");
}
/**
* Defines a regular expression for a setters name
*
* @return a compiled pattern for the setter's name
*/
protected Pattern getSetterPattern() {
return Pattern.compile("^set");
}
// ------------------->> equals() / hashcode() / toString()
// ------------------->> Inner classes
}