ValueReferenceScanner.java

package org.cyclopsgroup.caff.ref;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;

/**
 * A class that iterate through all property descriptors and fields and look for possible value
 * references.
 *
 * @author <a href="mailto:jiaqi.guo@gmail.com">Jiaqi Guo</a>
 * @param <T> Type of bean to inspect
 */
public class ValueReferenceScanner<T> {
  /**
   * A listener interface to handle found references
   *
   * @param <T> Type of bean to inspect
   * @param <H> Type of hint object as the second parameter
   */
  public static interface Listener<T, H> {
    /**
     * @param reference Found reference that meets requirement
     * @param hint A hint object
     * @param access Access point, a method or field, where the reference comes from
     */
    void handleReference(ValueReference<T> reference, H hint, AccessibleObject access);
  }

  private final Class<T> beanType;

  /**
   * @param beanType Type of bean to scan
   */
  public ValueReferenceScanner(Class<T> beanType) {
    if (beanType == null) {
      throw new NullPointerException("Given bean type can't be NULL");
    }
    this.beanType = beanType;
  }

  private <H extends Annotation> AccessibleObject findAnnotatedAccess(
      PropertyDescriptor desc, Class<H> expectedAnnotation) {
    Field field;
    try {
      field = beanType.getClass().getField(desc.getName());
    } catch (Throwable e) {
      field = null;
    }
    List<AccessibleObject> accesses =
        Arrays.asList((AccessibleObject) desc.getReadMethod(), desc.getWriteMethod(), field);
    for (AccessibleObject access : accesses) {
      if (access != null && access.isAnnotationPresent(expectedAnnotation)) {
        return access;
      }
    }
    return null;
  }

  /**
   * Scan given class and look for possible references annotated by given annotation
   *
   * @param <H> Only field or method annotated with this annotation is inspected
   * @param expectedAnnotation Only field or method annotated with this annotation is inspected
   * @param listener Listener to handle found references
   */
  public <H extends Annotation> void scanForAnnotation(
      Class<H> expectedAnnotation, Listener<T, H> listener) {
    BeanInfo beanInfo;
    try {
      beanInfo = Introspector.getBeanInfo(beanType);
    } catch (IntrospectionException e) {
      throw new RuntimeException("Can't get bean info of " + beanType);
    }
    for (PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
      AccessibleObject access = findAnnotatedAccess(desc, expectedAnnotation);
      if (access == null) {
        continue;
      }
      ValueReference<T> reference;
      if (access instanceof Field) {
        reference = ValueReference.instanceOf((Field) access);
      } else {
        reference = ValueReference.instanceOf(desc);
      }
      listener.handleReference(reference, access.getAnnotation(expectedAnnotation), access);
    }
    for (Field field : beanType.getFields()) {
      H annotation = field.getAnnotation(expectedAnnotation);
      if (annotation == null) {
        continue;
      }
      ValueReference<T> reference = ValueReference.instanceOf(field);
      listener.handleReference(reference, annotation, field);
    }
  }
}