org.tdar.core.service.ReflectionService.java Source code

Java tutorial

Introduction

Here is the source code for org.tdar.core.service.ReflectionService.java

Source

package org.tdar.core.service;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import org.tdar.core.bean.BulkImportField;
import org.tdar.core.bean.Obfuscatable;
import org.tdar.core.bean.Persistable;
import org.tdar.core.bean.resource.InformationResource;
import org.tdar.core.configuration.TdarConfiguration;
import org.tdar.core.dao.GenericDao;
import org.tdar.core.exception.TdarRecoverableRuntimeException;
import org.tdar.core.service.bulk.CellMetadata;
import org.tdar.utils.MessageHelper;
import org.tdar.utils.Pair;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionProxy;

/**
 * Service to help with Reflection
 * 
 * @author Adam Brin
 */
@Service
public class ReflectionService {

    private static final String EXECUTE = "execute";
    private static final String ORG_TDAR2 = "org/tdar/";
    private static final String SET = "set";
    private static final String GET = "get";
    private static final String ORG_TDAR = "org.tdar.";
    private transient Logger logger = LoggerFactory.getLogger(getClass());
    private static transient Logger staticLogger = LoggerFactory.getLogger(ReflectionService.class);
    private Map<String, Class<Persistable>> persistableLookup;

    @Autowired
    private GenericDao genericDao;

    /**
     * This method looks at a class like "Resource" and finds fields that contain the "classToFind",
     * e.g. GeographicKeyword. This would return [geographicKeywords,managedGeographicKeywords]
     * 
     * @param classToInspect
     * @param classToFind
     * @return
     */
    public Set<Field> findFieldsReferencingClass(Class<?> classToInspect, Class<?> classToFind) {
        Set<Field> matchingFields = new HashSet<>();
        for (Field field : classToInspect.getDeclaredFields()) {
            if (getFieldReturnType(field).equals(classToFind)) {
                matchingFields.add(field);
            }
        }
        logger.debug("Found Fields:{} on {}", matchingFields, classToInspect.getSimpleName());
        return matchingFields;
    }

    /**
     * Find all fields with a return-type of the specified class
     * 
     * @param classToInspect
     * @param ancestorToFind
     * @return
     */
    public Set<Field> findAssignableFieldsRefererencingClass(Class<?> classToInspect, Class<?> ancestorToFind) {
        Set<Field> matchingFields = new HashSet<>();
        for (Field field : classToInspect.getDeclaredFields()) {
            if (getFieldReturnType(field).isAssignableFrom(ancestorToFind)) {
                matchingFields.add(field);
            }
        }
        logger.debug("Fields in {} that refer to {}:{}",
                new Object[] { classToInspect.getSimpleName(), ancestorToFind.getSimpleName(), matchingFields });
        return matchingFields;
    }

    /**
     * Take the method name and try and replace it with the same
     * logic that Hibernate uses
     * 
     * @param name
     * @return
     */
    public static String cleanupMethodName(String name) {
        name = name.replaceAll("^(get|set)", "");
        name = name.substring(0, 1).toLowerCase() + name.substring(1);
        return name;
    }

    /**
     * String the getter or setter prefix from a method name to get the field name
     * 
     * @param method
     * @return
     */
    public static String cleanupMethodName(Method method) {
        return cleanupMethodName(method.getName());
    }

    /**
     * from the field, generate the appropriate Getter name
     * 
     * @param field
     * @return
     */
    public static String generateGetterName(Field field) {
        return generateGetterName(field.getName());
    }

    /**
     * From the string generate the getter name
     * 
     * @param name
     * @return
     */
    public static String generateGetterName(String name) {
        return generateName(GET, name);
    }

    /**
     * From the field, generate the Setter name
     * 
     * @param field
     * @return
     */
    public static String generateSetterName(Field field) {
        return generateSetterName(field.getName());
    }

    /**
     * From the String, generate the appropriate setter
     * 
     * @param name
     * @return
     */
    public static String generateSetterName(String name) {
        return generateName(SET, name);
    }

    /**
     * Based on the field and the object passed in, call the getter and return the result
     * 
     * @param obj
     * @param field
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T callFieldGetter(Object obj, Field field) {
        // logger.debug("calling getter on: {} {} ", obj, field.getName());
        logger.trace("{}", field.getDeclaringClass());
        Method method = ReflectionUtils.findMethod(field.getDeclaringClass(), generateGetterName(field));
        if (method.getReturnType() != Void.TYPE) {
            try {
                return (T) method.invoke(obj);
            } catch (Exception e) {
                logger.debug("cannot call field getter for field: {}", field, e);
            }
        }
        return null;
    }

    /**
     * Call the setter of the supplied object and field with the supplied value
     * 
     * @param obj
     * @param field
     * @param fieldValue
     */
    public <T> void callFieldSetter(Object obj, Field field, T fieldValue) {
        String setterName = generateSetterName(field);
        String valClass = "null";
        if (fieldValue != null) {
            valClass = fieldValue.getClass().getSimpleName();
        }
        logger.trace("Calling {}.{}({})",
                new Object[] { field.getDeclaringClass().getSimpleName(), setterName, valClass });
        // here we assume that field's type is assignable from the fieldValue
        Method setter = ReflectionUtils.findMethod(field.getDeclaringClass(), setterName, field.getType());
        try {
            setter.invoke(obj, fieldValue);
        } catch (Exception e) {
            logger.debug("cannot call field setter {} on : {} {}  {}", field, obj, fieldValue, e);
        }

    }

    /**
     * Get the CamelCase name for a field
     * 
     * @param prefix
     * @param name
     * @return
     */
    private static String generateName(String prefix, String name) {
        return prefix + name.substring(0, 1).toUpperCase() + name.substring(1);
    }

    /**
     * Get the return type of a field
     * 
     * @param accessibleObject
     * @return
     */
    public static Class<?> getFieldReturnType(AccessibleObject accessibleObject) {
        final Logger log = LoggerFactory.getLogger(ReflectionService.class);

        if (accessibleObject instanceof Field) {
            Field field = (Field) accessibleObject;
            log.trace("generic type: {}", field.getGenericType());
            return getType(field.getGenericType());
        }
        if (accessibleObject instanceof Method) {
            Method method = (Method) accessibleObject;
            log.trace("generic type: {}", method.getGenericReturnType());
            return getType(method.getGenericReturnType());
        }
        return null;
    }

    /**
     * Get the Class of the return type/or generic type
     * 
     * @param type
     * @return
     */
    private static Class<?> getType(Type type) {
        Logger logger = LoggerFactory.getLogger(ReflectionService.class);

        if (WildcardType.class.isAssignableFrom(type.getClass())) {
            WildcardType subType = (WildcardType) type;
            logger.trace(" wildcard type: {} [{}]", type, type.getClass());
            logger.trace(" lower: {} upper: {}", subType.getLowerBounds(), subType.getUpperBounds());
            return subType.getUpperBounds().getClass();
        }

        if (type instanceof ParameterizedType) {
            ParameterizedType collectionType = (ParameterizedType) type;
            logger.trace(" parameterized type: {} [{} - {}]", type, type.getClass(),
                    collectionType.getActualTypeArguments());
            Type subtype = collectionType.getActualTypeArguments()[0];
            logger.trace(" type: {} subtype: {} ", type, subtype);
            if (subtype instanceof Type) {
                return getType(subtype);
            }
            return (Class<?>) subtype;
        }

        if (type instanceof Class<?>) {
            return (Class<?>) type;
        }
        return null;
    }

    /**
     * Find Classes within the org/tdar/core/bean tree that support @link Persistable and resolve the SimpleName with the specified String
     * 
     * @param name
     * @return
     * @throws NoSuchBeanDefinitionException
     * @throws ClassNotFoundException
     */
    public Class<Persistable> getMatchingClassForSimpleName(String name) throws ClassNotFoundException {
        logger.trace("scanning for: {}", name);
        scanForPersistables();
        logger.trace("scanning in: {}", persistableLookup);
        return persistableLookup.get(name);
    }

    /**
     * Scan the class tree to find all objects that implement @link Persistable
     * 
     * @throws NoSuchBeanDefinitionException
     * @throws ClassNotFoundException
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void scanForPersistables() throws ClassNotFoundException {
        if (persistableLookup != null) {
            return;
        }

        Set<BeanDefinition> findCandidateComponents = findClassesThatImplement(Persistable.class);
        persistableLookup = new HashMap<>();
        for (BeanDefinition bd : findCandidateComponents) {
            String beanClassName = bd.getBeanClassName();
            Class cls = Class.forName(beanClassName);
            logger.trace("{} - {} ", cls.getSimpleName(), cls);
            if (persistableLookup.containsKey(cls.getSimpleName())) {
                throw new TdarRecoverableRuntimeException("reflectionService.jaxb_mapping",
                        Arrays.asList(cls.getSimpleName()));
            }
            persistableLookup.put(cls.getSimpleName(), cls);
        }

    }

    /**
     * Find all classes that implement the identified Class
     * 
     * @param cls
     * @return
     */
    public static Set<BeanDefinition> findClassesThatImplement(Class<?> cls) {
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(
                false);
        scanner.addIncludeFilter(new AssignableTypeFilter(cls));
        String basePackage = ORG_TDAR2;
        Set<BeanDefinition> findCandidateComponents = scanner.findCandidateComponents(basePackage);
        return findCandidateComponents;
    }

    private static Map<String, Boolean> annotationLookupCache = new ConcurrentHashMap<String, Boolean>();

    /**
     * Check whether the identified Method or Action has the annotation
     * 
     * @param invocation
     * @param annotationClass
     * @return
     * @throws SecurityException
     * @throws NoSuchMethodException
     */
    public static boolean methodOrActionContainsAnnotation(ActionInvocation invocation,
            Class<? extends Annotation> annotationClass) throws SecurityException, NoSuchMethodException {
        Object action = invocation.getAction();
        ActionProxy proxy = invocation.getProxy();
        String methodName = proxy.getMethod();
        Method method = null;

        if (methodName == null) {
            methodName = EXECUTE;
        }

        String key = annotationClass.getCanonicalName() + "|" + action.getClass().getCanonicalName() + "$"
                + methodName;
        Boolean found = annotationLookupCache.get(key);
        staticLogger.trace("key: {}, found: {}", key, found);
        if (found != null) {
            return found;
        }

        found = Boolean.FALSE;

        if (action != null) {
            method = action.getClass().getMethod(methodName);
        }

        if (method != null) {
            Object class_ = AnnotationUtils.findAnnotation(method.getDeclaringClass(), annotationClass);
            Object method_ = AnnotationUtils.findAnnotation(method, annotationClass);
            found = ((class_ != null) || (method_ != null));
        }

        Annotation parentClassAnnotation = AnnotationUtils.findAnnotation(action.getClass(), annotationClass);
        if (parentClassAnnotation != null) {
            found = true;
        }
        annotationLookupCache.put(key, found);

        return found;
    }

    /**
     * For the specified Method, return the annotation of the identified Class.
     * 
     * @param method
     * @param annotationClass
     * @return
     */
    public static <C extends Annotation> C getAnnotationFromMethodOrClass(Method method, Class<C> annotationClass) {
        C method_ = AnnotationUtils.findAnnotation(method, annotationClass);
        if (method_ != null) {
            return method_;
        }
        C class_ = AnnotationUtils.findAnnotation(method.getDeclaringClass(), annotationClass);
        if (class_ != null) {
            return class_;
        }
        return null;
    }

    /**
     * Find all classes or methods that have the identified annotation
     * 
     * @param method
     * @param annotationClass
     * @return
     */
    public static boolean classOrMethodContainsAnnotation(Method method,
            Class<? extends Annotation> annotationClass) {
        return getAnnotationFromMethodOrClass(method, annotationClass) != null;
    }

    /**
     * find all Classes that support the identified Annotation
     * 
     * @param annots
     * @return
     * @throws NoSuchBeanDefinitionException
     * @throws ClassNotFoundException
     */
    @SafeVarargs
    public static Class<?>[] scanForAnnotation(Class<? extends Annotation>... annots)
            throws ClassNotFoundException {
        List<Class<?>> toReturn = new ArrayList<>();
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(
                false);
        for (Class<? extends Annotation> annot : annots) {
            scanner.addIncludeFilter(new AnnotationTypeFilter(annot));
        }
        String basePackage = ORG_TDAR2;
        for (BeanDefinition bd : scanner.findCandidateComponents(basePackage)) {
            String beanClassName = bd.getBeanClassName();
            Class<?> cls = Class.forName(beanClassName);
            toReturn.add(cls);
        }
        return toReturn.toArray(new Class<?>[0]);
    }

    /**
     * Find all beans that implment the @link Persistable interface
     * 
     * @param cls
     * @return
     */
    @SuppressWarnings("unchecked")
    public List<Pair<Field, Class<? extends Persistable>>> findAllPersistableFields(Class<?> cls) {
        List<Field> declaredFields = new ArrayList<>();
        List<Pair<Field, Class<? extends Persistable>>> result = new ArrayList<>();
        // iterate up the package hierarchy
        while (cls.getPackage().getName().startsWith(ORG_TDAR)) {
            CollectionUtils.addAll(declaredFields, cls.getDeclaredFields());
            cls = cls.getSuperclass();
        }

        for (Field field : declaredFields) {
            Class<? extends Persistable> type = null;
            // generic collections
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())
                    || java.lang.reflect.Modifier.isTransient(field.getModifiers())
                    || java.lang.reflect.Modifier.isFinal(field.getModifiers())) {
                continue;
            }

            if (Collection.class.isAssignableFrom(field.getType())) {
                ParameterizedType stringListType = (ParameterizedType) field.getGenericType();
                if (Persistable.class.isAssignableFrom(
                        (Class<? extends Persistable>) stringListType.getActualTypeArguments()[0])) {
                    type = (Class<? extends Persistable>) stringListType.getActualTypeArguments()[0];
                    logger.trace("\t -> {}", type); // class java.lang.String.
                }
            }
            // singletons
            if (Persistable.class.isAssignableFrom(field.getType())) {
                type = (Class<? extends Persistable>) field.getType();
                logger.trace("\t -> {}", type); // class java.lang.String.
            }

            // things to add
            if (type != null) {
                result.add(new Pair<Field, Class<? extends Persistable>>(field, type));
            }
        }
        return result;
    }

    /**
     * @see #findBulkAnnotationsOnClass(Class, Stack, String)
     * 
     * @param class2
     * @return
     */
    public LinkedHashSet<CellMetadata> findBulkAnnotationsOnClass(Class<?> class2) {
        Stack<List<Class<?>>> classStack = new Stack<>();
        return findBulkAnnotationsOnClass(class2, classStack, "");
    }

    /**
     * Find all @link BulkImportField annotations on all resource classes.
     * 
     * @param class2
     * @param stack
     * @param prefix
     * @return
     */
    public LinkedHashSet<CellMetadata> findBulkAnnotationsOnClass(Class<?> class2, Stack<List<Class<?>>> stack,
            String prefix) {
        Class<BulkImportField> annotationToFind = BulkImportField.class;
        LinkedHashSet<CellMetadata> set = new LinkedHashSet<>();
        if (class2.getSuperclass() != Object.class) {
            set.addAll(findBulkAnnotationsOnClass(class2.getSuperclass(), stack, prefix));
        }

        Field runMultiple = null;
        List<Class<?>> runWith = new ArrayList<Class<?>>();
        for (Field field : class2.getDeclaredFields()) {
            BulkImportField annotation = field.getAnnotation(annotationToFind);
            if ((annotation != null) && ArrayUtils.isNotEmpty(annotation.implementedSubclasses())) {
                runWith.addAll(Arrays.asList(annotation.implementedSubclasses()));
                runMultiple = field;
            }
        }
        List<Class<?>> classList = new ArrayList<Class<?>>();
        stack.add(classList);
        classList.add(class2);

        if (runMultiple == null) {
            set.addAll(handleClassAnnotations(class2, stack, annotationToFind, null, null, prefix));
        } else {
            for (Class<?> runAs : runWith) {
                classList.add(runAs);
                set.addAll(handleClassAnnotations(class2, stack, annotationToFind, runAs, runMultiple, prefix));
                classList.remove(runAs);
            }
        }
        stack.remove(classList);
        return set;
    }

    /**
     * Find @link BulkImportField on a class.
     * 
     * @param class2
     * @param stack
     * @param annotationToFind
     * @param runAs
     * @param runAsField
     * @param prefix
     * @return
     */
    private LinkedHashSet<CellMetadata> handleClassAnnotations(Class<?> class2, Stack<List<Class<?>>> stack,
            Class<BulkImportField> annotationToFind, Class<?> runAs, Field runAsField, String prefix) {
        LinkedHashSet<CellMetadata> set = new LinkedHashSet<>();
        for (Field field : class2.getDeclaredFields()) {
            BulkImportField annotation = field.getAnnotation(annotationToFind);
            if (prefix == null) {
                prefix = "";
            }
            if (annotation != null) {
                String fieldPrefix = prefix;
                if (StringUtils.isNotBlank(annotation.key())) {
                    fieldPrefix = CellMetadata.getDisplayLabel(MessageHelper.getInstance(), annotation.key());
                }

                Class<?> type = field.getType();
                if (Objects.equals(field, runAsField)) {
                    type = runAs;
                    logger.trace(" ** overriding type with {}", type.getSimpleName());
                }

                if (Collection.class.isAssignableFrom(type))
                // handle Collection private List<ResourceCreator> ...
                {
                    ParameterizedType stringListType = (ParameterizedType) field.getGenericType();
                    Class<?> cls = (Class<?>) stringListType.getActualTypeArguments()[0];
                    set.addAll(findBulkAnnotationsOnClass(cls, stack, fieldPrefix));
                }
                // handle Singleton private Person owner ...
                else if (Persistable.class.isAssignableFrom(type)) {
                    set.addAll(findBulkAnnotationsOnClass(type, stack, fieldPrefix));
                }
                // handle more primative fields private String ...
                else {
                    logger.trace("adding {} ({})", field, stack);
                    if (!TdarConfiguration.getInstance().getCopyrightMandatory()
                            && Objects.equals(annotation.key(), InformationResource.COPYRIGHT_HOLDER)) {
                        continue;
                    }

                    if ((TdarConfiguration.getInstance().getLicenseEnabled() == false)
                            && (Objects.equals(field.getName(), "licenseType")
                                    || Objects.equals(field.getName(), "licenseText"))) {
                        continue;
                    }
                    set.add(new CellMetadata(field, annotation, class2, stack, prefix));

                    // set.add(field);
                }

            }
        }
        return set;
    }

    /**
     * Cast the value to the object type of the field and call the setter. Validate the propert in the process.
     * 
     * @param beanToProcess
     * @param name
     * @param value
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void validateAndSetProperty(Object beanToProcess, String name, String value) {
        List<String> errorValueList = Arrays.asList(name, value);
        try {
            logger.trace("processing: {} - {} --> {}", beanToProcess, name, value);
            Class propertyType = PropertyUtils.getPropertyType(beanToProcess, name);

            // handle types should we be testing column length?
            if (propertyType.isEnum()) {
                try {
                    BeanUtils.setProperty(beanToProcess, name, Enum.valueOf(propertyType, value));
                } catch (IllegalArgumentException e) {
                    logger.debug("cannot set property:", e);
                    throw new TdarRecoverableRuntimeException("reflectionService.not_valid_value", e,
                            errorValueList);
                }
            } else {
                if (Integer.class.isAssignableFrom(propertyType)) {
                    try {
                        Double dbl = Double.valueOf(value);
                        if (dbl == Math.floor(dbl)) {
                            value = new Integer((int) Math.floor(dbl)).toString();
                        }
                    } catch (NumberFormatException nfe) {
                        throw new TdarRecoverableRuntimeException("reflectionService.expecting_integer",
                                errorValueList);
                    }
                }
                if (Long.class.isAssignableFrom(propertyType)) {
                    try {
                        Double dbl = Double.valueOf(value);
                        if (dbl == Math.floor(dbl)) {
                            value = new Long((long) Math.floor(dbl)).toString();
                        }
                    } catch (NumberFormatException nfe) {
                        throw new TdarRecoverableRuntimeException("reflectionService.expecting_big_integer",
                                errorValueList);
                    }
                }
                if (Float.class.isAssignableFrom(propertyType)) {
                    try {
                        Float.parseFloat(value);
                    } catch (NumberFormatException nfe) {
                        throw new TdarRecoverableRuntimeException("reflectionService.expecting_floating_point",
                                errorValueList);
                    }
                }
                BeanUtils.setProperty(beanToProcess, name, value);
            }
        } catch (Exception e1) {
            if (e1 instanceof TdarRecoverableRuntimeException) {
                throw (TdarRecoverableRuntimeException) e1;
            }
            logger.debug("error processing bulk upload: {}", e1);
            throw new TdarRecoverableRuntimeException("reflectionService.expecting_generic", errorValueList);
        }
    }

    /**
     * Find all getters of beans that support the @link Obfuscatable interface and any child beans throughout the graph
     * 
     * @param cls
     * @return
     */
    @SuppressWarnings("unchecked")
    public List<Pair<Method, Class<? extends Obfuscatable>>> findAllObfuscatableGetters(Class<?> cls) {
        List<Method> declaredFields = new ArrayList<>();
        List<Pair<Method, Class<? extends Obfuscatable>>> result = new ArrayList<>();
        // iterate up the package hierarchy
        Class<?> actualClass = null;
        while (cls.getPackage().getName().startsWith(ORG_TDAR)) {
            // find first implemented tDAR class (actual class);
            if (actualClass == null) {
                actualClass = cls;
            }
            for (Method method : cls.getDeclaredMethods()) {

                if (Modifier.isPublic(method.getModifiers()) && method.getName().startsWith(GET)) {
                    declaredFields.add(method);
                }
            }
            cls = cls.getSuperclass();
        }

        for (Method method : declaredFields) {
            Class<? extends Obfuscatable> type = null;
            // generic collections
            if (java.lang.reflect.Modifier.isStatic(method.getModifiers())
                    || java.lang.reflect.Modifier.isTransient(method.getModifiers())
                    || java.lang.reflect.Modifier.isFinal(method.getModifiers())) {
                continue;
            }

            // logger.info("TYPE: {} {} ", method.getGenericReturnType(), method.getName());
            // logger.info("{} ==> {}", actualClass, method.getDeclaringClass());
            // logger.info(" {} {} {} ", dcl.getTypeParameters(), dcl.getGenericInterfaces(), dcl.getGenericSuperclass());
            boolean force = false;
            if (Collection.class.isAssignableFrom(method.getReturnType())) {
                Class<?> type2 = getType(method.getGenericReturnType());
                if (type2 == null) {
                    force = true;
                } else if (Obfuscatable.class.isAssignableFrom(type2)) {
                    type = (Class<? extends Obfuscatable>) type2;
                    logger.trace("\t -> {}", type); // class java.lang.String.
                }
            }
            // singletons
            if (Obfuscatable.class.isAssignableFrom(method.getReturnType())) {
                type = (Class<? extends Obfuscatable>) method.getReturnType();
                logger.trace("\t -> {}", type); // class java.lang.String.
            }

            // things to add
            if ((type != null) || force) {
                if (force) {
                    logger.trace(
                            "forcing method to be obfuscated because cannot figure out gneric type {} (good luck)",
                            method);
                }
                result.add(new Pair<Method, Class<? extends Obfuscatable>>(method, type));
            }
        }
        return result;
    }

    public static List<Field> findAnnotatedFieldsOfClass(Class<?> cls,
            Class<? extends Annotation> annotationClass) {
        List<Field> result = new ArrayList<>();
        // iterate up the package hierarchy
        Class<?> actualClass = null;
        while (cls.getPackage().getName().startsWith(ORG_TDAR)) {
            // find first implemented tDAR class (actual class);
            if (actualClass == null) {
                actualClass = cls;
            }
            for (Field field : cls.getDeclaredFields()) {
                Object annotation = field.getAnnotation(annotationClass);
                if (annotation != null) {
                    result.add(field);
                }
            }
            cls = cls.getSuperclass();
        }
        return result;
    }

    public void walkObject(Persistable p) {
        logger.debug("{} {}", p.getClass().getCanonicalName(), p);
        Set<String> seen = new HashSet<>();
        walkObject(p, 0, seen);
    }

    private String makeKey(Persistable p) {
        return String.format("%s-%s", p.getClass().getSimpleName(), p.getId());
    }

    private void walkObject(Persistable p, int indent, Set<String> seen) {
        List<Pair<Field, Class<? extends Persistable>>> findAllPersistableFields = findAllPersistableFields(
                p.getClass());
        String key = makeKey(p);
        if (seen.contains(key)) {
            logger.debug("{}[{}] {}", StringUtils.repeat("| ", indent + 1), "seen", p);
            return;
        }
        seen.add(key);
        for (Pair<Field, Class<? extends Persistable>> pair : findAllPersistableFields) {
            Object content = callFieldGetter(p, pair.getFirst());
            if (content == null) {
                logger.trace("{}{}", StringUtils.repeat("| ", indent + 1), pair.getFirst());
                continue;
            }
            logger.trace("{}, {}", content, pair.getFirst());
            if (Collection.class.isAssignableFrom(content.getClass())) {
                @SuppressWarnings("unchecked")
                Collection<Persistable> originalList = (Collection<Persistable>) content;
                Collection<Persistable> contents = new ArrayList<Persistable>(originalList);
                // using a separate collection to avoid concurrent modification of bi-directional double-lists
                if (CollectionUtils.isNotEmpty(contents)) {
                    logger.debug("{}{}", StringUtils.repeat("| ", indent + 1), pair.getFirst().getName());
                }
                Iterator<Persistable> iterator = contents.iterator();
                while (iterator.hasNext()) {
                    Persistable p_ = iterator.next();
                    boolean sessionContains = genericDao.sessionContains(p_);
                    logger.debug("{}[{}] {}", StringUtils.repeat("| ", indent + 2), sessionContains, p_);
                    if (sessionContains) {
                        walkObject(p_, indent + 2, seen);
                    }
                }
            } else {
                boolean sessionContains = genericDao.sessionContains(content);
                logger.debug("{}[{}] {} {}", StringUtils.repeat("| ", indent + 1), sessionContains,
                        pair.getFirst().getName(), content);
                if (sessionContains) {
                    walkObject((Persistable) content, indent + 1, seen);
                }
            }
        }
    }

    public Method findMatchingSetter(Method method) {
        String name = "set" + method.getName().substring(3);
        return ReflectionUtils.findMethod(method.getDeclaringClass(), name, method.getReturnType());
    }

    public Field getFieldForGetterOrSetter(Method method) {
        String name = cleanupMethodName(method);
        Field field = ReflectionUtils.findField(method.getDeclaringClass(), name);
        return field;
    }

}