BasePojomator.java

package org.pojomatic.internal;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;

import org.pojomatic.Pojomator;
import org.pojomatic.PropertyElement;

public abstract class BasePojomator<T> implements Pojomator<T> {
  protected final Class<?> pojoClass;
  private final ClassProperties classProperties;

  protected BasePojomator(Class<?> pojoClass, ClassProperties classProperties) {
    this.pojoClass = pojoClass;
    this.classProperties = classProperties;
  }

  @Override
  public boolean isCompatibleForEquality(Class<?> otherClass) {
    return classProperties.isCompatibleForEquals(otherClass);
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("Pojomator for ").append(pojoClass.getName()).append(" with equals properties ");
    propertiesList(builder, classProperties.getEqualsProperties());
    builder.append(", hashCodeProperties ");
    propertiesList(builder, classProperties.getHashCodeProperties());
    builder.append(", and toStringProperties ");
    propertiesList(builder, classProperties.getToStringProperties());
    return builder.toString();
  }

  private void propertiesList(StringBuilder builder, final Iterable<PropertyElement> properties) {
    builder.append("{");
    boolean firstElement = true;
    for (PropertyElement prop: properties) {
      if (!firstElement) {
        builder.append(",");
      }
      else {
        firstElement = false;
      }
      builder.append(prop.getName());
    }
    builder.append("}");
  }

  /**
   * Construct a call site for a property accessor. Because {@code pojoClass} might not be a public class, the
   * parameter in {@code methodType} cannot be {@code pojoClass}, but instead must be just {@code Object.class}. The
   * {@code pojoClass} parameter will be stored as static field in the Pojomator class, and passed in from it's
   * bootstrap method.
   * @param caller A Lookup from the original call site.
   * @param name the name of the dynamic method. This should either be "field_&lt;fieldName&gt;" or "method_&lt;methodName&gt;".
   * @param methodType the type of the dynamic method; the return type should be the type of the aforementioned field
   *   or method
   * @param pojomatorClass the type of the pojomator class
   * @return a CallSite which invokes the method or gets the field value.
   * @throws Throwable if there are reflection issues
   */
  protected static CallSite bootstrap(
      MethodHandles.Lookup caller, String name, MethodType methodType, Class<?> pojomatorClass)
      throws Throwable {
    return new ConstantCallSite(
      MethodHandles.explicitCastArguments(
        getTypedMethod(caller, name, pojomatorClass),
        MethodType.methodType(methodType.returnType(), Object.class)));
  }

  /**
   * Compare two values of static type Object for equality. If both values are arrays, then they will be considered
   * equal iff they have the same class, and (recursively) an equal set of elements.
   * @param instanceValue the first value to compare
   * @param otherValue the second value to compare
   * @return true if {@code instanceValue} and {@code otherValue} are equal to each other.
   */
  protected static boolean areObjectValuesEqual(Object instanceValue, Object otherValue) {
    if (instanceValue == otherValue) {
      return true;
    }
    if (instanceValue == null || otherValue == null) {
      return false;
    }
    else {
      if (!instanceValue.getClass().isArray()) {
        if (!instanceValue.equals(otherValue)) {
          return false;
        }
      }
      else {
        return compareArrays(instanceValue, otherValue);
      }
    }
    return true;
  }

  /**
   * Compare two values of array type for equality. They will be considered
   * equal iff they have the same class, and (recursively) an equal set of elements.
   * @param instanceValue the first value to compare
   * @param otherValue the second value to compare
   * @return true if {@code instanceValue} and {@code otherValue} are equal to each other.
   */
  protected static boolean compareArrays(Object instanceValue, Object otherValue) {
    if (instanceValue == otherValue) {
      return true;
    }
    if (instanceValue == null || otherValue == null) {
      return false;
    }
    if (!instanceValue.getClass().equals(otherValue.getClass())) {
      return false;
    }
    final Class<?> instanceComponentClass = instanceValue.getClass().getComponentType();

    if (!instanceComponentClass.isPrimitive()) {
      return Arrays.deepEquals((Object[]) instanceValue, (Object[]) otherValue);
    }
    else { // instanceComponentClass is primitive
      if (Boolean.TYPE == instanceComponentClass) {
        return Arrays.equals((boolean[]) instanceValue, (boolean[]) otherValue);
      }
      else if (Byte.TYPE == instanceComponentClass) {
        return Arrays.equals((byte[]) instanceValue, (byte[]) otherValue);
      }
      else if (Character.TYPE == instanceComponentClass) {
        return Arrays.equals((char[]) instanceValue, (char[]) otherValue);
      }
      else if (Short.TYPE == instanceComponentClass) {
        return Arrays.equals((short[]) instanceValue, (short[]) otherValue);
      }
      else if (Integer.TYPE == instanceComponentClass) {
        return Arrays.equals((int[]) instanceValue, (int[]) otherValue);
      }
      else if (Long.TYPE == instanceComponentClass) {
        return Arrays.equals((long[]) instanceValue, (long[]) otherValue);
      }
      else if (Float.TYPE == instanceComponentClass) {
        return Arrays.equals((float[]) instanceValue, (float[]) otherValue);
      }
      else if (Double.TYPE == instanceComponentClass) {
        return Arrays.equals((double[]) instanceValue, (double[]) otherValue);
      }
      else {
        // should NEVER happen
        throw new IllegalStateException(
          "unknown primitive type " + instanceComponentClass.getName());
      }
    }
  }


  /**
   * Given an object which is of array type, compute it's hashCode by calling the appropriate signature of
   * {@link Arrays}{@code .hashCode()}
   * @param array the array
   * @param deepArray whether to do a deep hashCode for Object arrays.
   * @return the hashCode
   */
  protected static int arrayHashCode(Object array, boolean deepArray) {
    Class<?> componentType = array.getClass().getComponentType();
    if (! componentType.isPrimitive()) {
      return deepArray ? Arrays.deepHashCode((Object[]) array) : Arrays.hashCode((Object[]) array);
    }
    if (componentType == boolean.class) {
      return Arrays.hashCode((boolean[]) array);
    }
    if (componentType == byte.class) {
      return Arrays.hashCode((byte[]) array);
    }
    if (componentType == char.class) {
      return Arrays.hashCode((char[]) array);
    }
    if (componentType == short.class) {
      return Arrays.hashCode((short[]) array);
    }
    if (componentType == int.class) {
      return Arrays.hashCode((int[]) array);
    }
    if (componentType == long.class) {
      return Arrays.hashCode((long[]) array);
    }
    if (componentType == float.class) {
      return Arrays.hashCode((float[]) array);
    }
    if (componentType == double.class) {
      return Arrays.hashCode((double[]) array);
    }
    throw new IllegalStateException("unknown primitive type " + componentType.getName());
  }

  protected static <T> T checkNotNull(T reference) {
    if (reference == null) {
      throw new NullPointerException();
    }
    return reference;
  }

  protected static <T> T checkNotNull(T reference, String message) {
    if (reference == null) {
      throw new NullPointerException(message);
    }
    return reference;
  }

  protected static void checkNotNullPop(Object reference) {
    if (reference == null) {
      throw new NullPointerException();
    }
  }

  protected void checkCompatibleForEquality(T instance, String label) {
    if (!isCompatibleForEquality(instance.getClass())) {
      throw new IllegalArgumentException(
        label + " has type " + instance.getClass().getName()
        + " which is not compatible for equality with " + pojoClass.getName());
    }
  }

  /**
   * Get a method handle to access a field or invoke a no-arg method.
   * @param caller A Lookup from the original call site.
   * @param name the name of the dynamic method. This should be of the form "get_xxx", where "element_xxx" will be a
   * static field containing a {@link PropertyElement} instance referring to the property to be accessed.
   * @param pojomatorClass the type of the pojomator class
   * @return the MethodHandle
   * @throws Throwable
   */
  private static MethodHandle getTypedMethod(
    final MethodHandles.Lookup caller, final String name, final Class<?> pojomatorClass)
    throws Throwable {
    try {
      return AccessController.doPrivileged(new PrivilegedExceptionAction<MethodHandle>() {
        @Override
        public MethodHandle run() throws Exception {
          return getTypedMethodPrivileged(caller, name, pojomatorClass);
        }
      });
    } catch (PrivilegedActionException e) {
      throw e.getCause();
    }
  }

  /**
   * Do the work for {@link #getTypedMethod(java.lang.invoke.MethodHandles.Lookup, String, Class)}. This method will be
   * run inside of a {@link AccessController#doPrivileged(PrivilegedExceptionAction)} block, hence should make sure to
   * not run untrusted code.
   * @param pojomatorClass the type of the pojomator class
   * @return the MethodHandle
   * @throws NoSuchFieldException
   * @throws IllegalAccessException
   */
  private static MethodHandle getTypedMethodPrivileged(
    MethodHandles.Lookup caller, String name, Class<?> pojomatorClass)
    throws NoSuchFieldException, IllegalAccessException {
    String elementName = "element_" + name.substring(4);
    Field elementField = pojomatorClass.getDeclaredField(elementName);
    elementField.setAccessible(true);
    PropertyElement property = (PropertyElement) elementField.get(null);
    AnnotatedElement element = property.getElement();
    // Note that while element is a reference to untrusted code, we do not actually invoke this code inside a
    // doPrivileged block - we merely make it accessible to be invoked later, outside of a doPriviliged block
    if (element instanceof Field) {
      Field field = (Field) element;
      field.setAccessible(true);
      return caller.unreflectGetter(field);
    }
    else if (element instanceof Method) {
      Method method = (Method) element;
      method.setAccessible(true);
      return caller.unreflect(method);
    }
    else {
      throw new IllegalArgumentException("Cannot handle element of type " + element.getClass().getName());
    }
  }

}